c ++ 11 — Существуют ли какие-либо языковые препятствия C ++, которые мешают переходу на D-диапазоны?

Это перекрестный вопрос C ++ / D. D язык программирования имеет диапазоны что в отличие от библиотек C ++, таких как Boost.Range— не основаны на парах итераторов. Официальный C ++ Ranges Study Group кажется, увязли в гвоздях технической спецификации.

Вопрос: есть ли у нынешнего C ++ 11 или будущего стандарта C ++ 14 какие-либо препятствия, препятствующие принятию диапазонов D, а также версии с соответствующим диапазоном <algorithm>— оптовая?

Я не знаю D или его диапазонов достаточно хорошо, но они кажутся ленивыми и сочетаемыми, а также способны предоставить расширенный набор алгоритмов STL. Учитывая их претензию на успех для D, было бы очень неплохо иметь библиотеку для C ++. Интересно, насколько важны уникальные возможности D (например, строковые миксины, унифицированный синтаксис вызова функций) для реализации его диапазонов и может ли C ++ имитировать это без особых усилий (например, C ++ 14 constexpr похоже, очень похоже на оценку функции времени компиляции D)

Примечание. Я ищу технические ответы, а не мнения о том, являются ли диапазоны D подходящими для использования в качестве библиотеки C ++.

17

Решение

Я не думаю, что есть какая-то присущая технический ограничение в C ++, которое сделало бы невозможным определение системы диапазонов в стиле D и соответствующих алгоритмов в C ++. Самой большой проблемой на уровне языка будет то, что C ++ основан на диапазоне forпетли требуют, чтобы begin() а также end() может использоваться в диапазонах, но при условии, что мы перейдем к длине определения библиотеки с использованием диапазонов в стиле D, расширяющей диапазон for-циклы, чтобы иметь дело с ними, кажется незначительным изменением.

Основная техническая проблема, с которой я столкнулся при экспериментировании с алгоритмами на диапазонах D-стиля в C ++, заключалась в том, что я не мог создавать алгоритмы так же быстро, как мои реализации на основе итераторов (фактически, курсоров). Конечно, это могут быть только мои реализации алгоритмов, но я не видел, чтобы кто-нибудь предоставил разумный набор алгоритмов на основе диапазона в D-стиле в C ++, против которого я мог бы профилировать. Производительность важна, и стандартная библиотека C ++ должна обеспечивать, по крайней мере, слабо эффективные реализации алгоритмов (общая реализация алгоритма называется слабо эффективный если при применении к структуре данных это, по крайней мере, так же быстро, как и при пользовательской реализации того же алгоритма, использующего ту же структуру данных с использованием того же языка программирования). Я не смог создать слабо эффективные алгоритмы на основе диапазонов D-стиля, и моя цель на самом деле сильно эффективный алгоритмы (похожи на слабо эффективные, но допускающие любой язык программирования и использующие только те же базовые аппаратные средства).

При экспериментировании с алгоритмами на основе диапазонов в стиле D я обнаружил, что алгоритмы реализовать гораздо сложнее, чем алгоритмы на основе итераторов, и счел необходимым иметь дело с кладжами, чтобы обойти некоторые их ограничения. Конечно, не все в нынешнем виде, как алгоритмы определены в C ++, также идеально. Грубый план того, как я хочу изменить алгоритмы и абстракции, с которыми они работают STL 2.0 стр. Однако эта страница не имеет большого отношения к диапазонам, поскольку это связанная, но несколько иная тема. Я предпочел бы представить диапазоны на основе итераторов (ну, в действительности, курсора), а не диапазонов в стиле D, но вопрос был не об этом.

Одна техническая проблема, с которой сталкиваются все абстракции диапазона в C ++, заключается в том, чтобы иметь дело с временными объектами разумным способом. Например, рассмотрим это выражение:

auto result = ranges::unique(ranges::sort(std::vector<int>{ read_integers() }));

В зависимости от того ranges::sort() или же ranges::unique() ленивы или нет, с представлением временного диапазона нужно разобраться. Простое предоставление представления об исходном диапазоне не подходит ни для одного из этих алгоритмов, поскольку временный объект исчезнет в конце выражения. Одной из возможностей может быть перемещение диапазона, если он входит в качестве r-значения, что требует разных результатов для обоих ranges::sort() а также ranges::unique() различать случаи, когда фактическим аргументом является либо временный объект, либо объект, поддерживаемый независимо. У D нет этой конкретной проблемы, потому что он собирает мусор, и диапазон источника, таким образом, будет поддерживаться в любом случае.

Вышеприведенный пример также показывает одну из проблем, связанных с алгоритмом вычисления с отложенным вычислением: поскольку любой тип, включая типы, которые не могут быть прописаны иначе, может быть выведен auto переменные или шаблонные функции, нет ничего, что заставляло бы ленивую оценку в конце выражения. Таким образом, результаты из шаблонов выражений могут быть получены, и алгоритм на самом деле не выполняется. То есть, если в алгоритм передается l-значение, необходимо убедиться, что выражение действительно вычислено для получения фактического эффекта. Например, любой sort() Алгоритм мутации всей последовательности четко выполняет мутацию на месте (если вы хотите, чтобы версия не делала это на месте, просто скопируйте контейнер и примените версию на месте; если у вас есть только не-версия, вы не может избежать дополнительной последовательности, которая может быть непосредственной проблемой, например, для гигантских последовательностей). Предполагая, что это в некотором роде ленивый доступ l-значения к исходной последовательности обеспечивает пик в текущем состоянии, что почти наверняка плохо. Это может означать, что ленивая оценка алгоритмов мутации в любом случае не такая уж хорошая идея.

В любом случае, есть некоторые аспекты C ++, которые делают невозможным немедленное принятие диапазонов D-sytle, хотя те же соображения применимы и к другим абстракциям диапазонов. Я думаю, что эти соображения, таким образом, несколько выходят за рамки этого вопроса. Кроме того, очевидное «решение» первой из проблем (добавление сборки мусора) вряд ли произойдет. Я не знаю, есть ли решение второй проблемы в D. Может появиться решение второй проблемы (предварительно названо оператор авто) но я не знаю ни о конкретном предложении, ни о том, как на самом деле будет выглядеть такая функция.

Кстати, Исследовательская группа по диапазонам на самом деле не увязла в технических деталях. До сих пор мы просто пытались выяснить, какие проблемы мы на самом деле пытаемся решить, и в некоторой степени расширить область решения. Кроме того, группы вообще не выполняют никакой работы! Фактическая работа всегда выполняется отдельными людьми, часто очень немногими. Поскольку основная часть работы на самом деле является разработкой набора абстракций, я ожидаю, что основы любых результатов Исследовательской группы по диапазонам будут составлять от 1 до 3 человек, которые имеют некоторое представление о том, что необходимо и как это должно выглядеть.

9

Другие решения

Мои знания C ++ 11 гораздо более ограничены, чем хотелось бы, поэтому могут быть более новые функции, которые улучшают вещи, о которых я еще не знаю, но есть три области, о которых я могу думать в данный момент которые по крайней мере проблематичны: шаблонные ограничения, static ifи введите самоанализ.

В D функция, основанная на диапазоне, обычно имеет шаблонное ограничение, указывающее, какой тип диапазонов она принимает (например, прямой диапазон по сравнению с диапазоном произвольного доступа). Например, вот упрощенная подпись для std.algorithm.sort:

auto sort(alias less = "a < b", Range)(Range r)
if(isRandomAccessRange!Range &&
hasSlicing!Range &&
hasLength!Range)
{...}

Он проверяет, что передаваемый тип является диапазоном произвольного доступа, что он может быть разрезан и что он имеет length имущество. Любой тип, который не удовлетворяет этим требованиям, не будет соответствовать sortи когда ограничение шаблона не удается, это дает понять программисту, почему их тип не будет работать с sort (вместо того, чтобы просто выдавать неприятную ошибку компилятора из середины шаблонной функции, когда она не может скомпилироваться с данным типом).

Теперь, хотя это может показаться улучшением юзабилити по сравнению с простым сообщением об ошибке компиляции, когда sort не может скомпилироваться, потому что тип не имеет правильных операций, он на самом деле оказывает большое влияние на перегрузку функций, а также на самоанализ типа. Например, вот два из std.algorithm.findперегрузки:

R find(alias pred = "a == b", R, E)(R haystack, E needle)
if(isInputRange!R &&
is(typeof(binaryFun!pred(haystack.front, needle)) : bool))
{...}R1 find(alias pred = "a == b", R1, R2)(R1 haystack, R2 needle)
if(isForwardRange!R1 && isForwardRange!R2 &&
is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool) &&
!isRandomAccessRange!R1)
{...}

Первый принимает иглу, которая является только одним элементом, тогда как второй принимает иглу, которая находится в переднем диапазоне. Эти два типа могут иметь разные типы параметров, основанные исключительно на ограничениях шаблона, и могут иметь радикально различный код внутри. Без чего-то вроде шаблонных ограничений вы не можете иметь шаблонные функции, которые перегружены на атрибуты своих аргументов (в отличие от перегрузки на самих конкретных типах), что значительно усложняет (если не делает невозможным) различные реализации, основанные на жанр используемого диапазона (например, диапазон ввода или прямой диапазон) или другие атрибуты используемых типов. В этой области в C ++ была проделана определенная работа с концепциями и похожими идеями, но AFAIK, C ++ по-прежнему серьезно не хватает функций, необходимых для перегрузки шаблонов (будь то шаблонные функции или шаблонные типы), основанных скорее на атрибутах их типов аргументов. чем специализация на конкретных типах аргументов (как это происходит со специализацией шаблона).

Связанная особенность будет static if, Это так же, как ifза исключением того, что его состояние оценивается во время компиляции, и true или же false фактически определит, какая ветка компилируется, а не какая ветка запускается. Это позволяет вам ветвить код на основе условий, известных во время компиляции. например

static if(isDynamicArray!T)
{}
else
{}

или же

static if(isRandomAccessRange!Range)
{}
else static if(isBidirectionalRange!Range)
{}
else static if(isForwardRange!Range)
{}
else static if(isInputRange!Range)
{}
else
static assert(0, Range.stringof ~ " is not a valid range!");

static if может в некоторой степени избавить от необходимости шаблонных ограничений, поскольку вы можете по существу поместить перегрузки для шаблонной функции в одну функцию. например

R find(alias pred = "a == b", R, E)(R haystack, E needle)
{
static if(isInputRange!R &&
is(typeof(binaryFun!pred(haystack.front, needle)) : bool))
{...}
else static if(isForwardRange!R1 && isForwardRange!R2 &&
is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool) &&
!isRandomAccessRange!R1)
{...}
}

но это все равно приводит к более неприятным ошибкам при сбое компиляции и фактически делает его таким, что вы не можете перегрузить шаблон (по крайней мере, с помощью реализации D), потому что перегрузка определяется до того, как будет создан экземпляр шаблона. Итак, вы можете использовать static if чтобы специализировать части реализации шаблона, но это не совсем дает вам достаточно того, какие ограничения шаблона заставляют вас не нуждаться в ограничениях шаблона (или что-то подобное).

Скорее, static if отлично подходит для таких вещей, как специализация только части реализации вашей функции или для того, чтобы тип диапазона мог должным образом наследовать атрибуты типа диапазона, который он переносит. Например, если вы позвоните std.algorithm.map в массиве целых чисел результирующий диапазон может иметь нарезку (потому что диапазон источника имеет), тогда как если вы вызвали map в диапазоне, в котором не было срезов (например, диапазоны, возвращаемые std.algorithm.filter не может иметь срезы), тогда результирующие диапазоны не будут иметь срезов. Чтобы сделать это, map использования static if скомпилировать в opSlice только когда диапазон источника поддерживает это. В настоящее время, map код, который делает это выглядит

static if (hasSlicing!R)
{
static if (is(typeof(_input[ulong.max .. ulong.max])))
private alias opSlice_t = ulong;
else
private alias opSlice_t = uint;

static if (hasLength!R)
{
auto opSlice(opSlice_t low, opSlice_t high)
{
return typeof(this)(_input[low .. high]);
}
}
else static if (is(typeof(_input[opSlice_t.max .. $])))
{
struct DollarToken{}
enum opDollar = DollarToken.init;
auto opSlice(opSlice_t low, DollarToken)
{
return typeof(this)(_input[low .. $]);
}

auto opSlice(opSlice_t low, opSlice_t high)
{
return this[low .. $].take(high - low);
}
}
}

Это код в определении типа mapтип возвращаемого значения и то, компилируется ли этот код или нет, полностью зависит от результатов static ifs, ни одна из которых не может быть заменена специализациями шаблонов на основе определенных типов без необходимости написания нового специализированного шаблона для map для каждого нового типа, который вы используете с ним (что явно не подходит). Чтобы компилировать код, основанный на атрибутах типов, а не на конкретных типах, вам действительно нужно что-то вроде static if (который C ++ в настоящее время не имеет).

Третий важный элемент, который отсутствует в C ++ (и который я более или менее затрагивал) — это интроспекция типов. Тот факт, что вы можете сделать что-то вроде is(typeof(binaryFun!pred(haystack.front, needle)) : bool) или же isForwardRange!Range это важно. Без возможности проверить, имеет ли конкретный тип определенный набор атрибутов или что конкретный фрагмент кода компилируется, вы даже не можете написать условия, какие ограничения шаблона и static if использовать. Например, std.range.isInputRange выглядит примерно так

template isInputRange(R)
{
enum bool isInputRange = is(typeof(
{
R r = void;       // can define a range object
if (r.empty) {}   // can test for empty
r.popFront();     // can invoke popFront()
auto h = r.front; // can get the front of the range
}));
}

Он проверяет, что определенный фрагмент кода компилируется для данного типа. Если это так, то этот тип можно использовать в качестве входного диапазона. Если это не так, то не может. AFAIK, невозможно сделать что-либо даже смутно, как это в C ++. Но чтобы разумно реализовать диапазоны, вам действительно нужно уметь isInputRange или проверить, компилируется ли определенный тип с sortis(typeof(sort(myRange))), Без этого вы не можете специализировать реализации, основанные на том, какие типы операций поддерживает определенный диапазон, вы не можете должным образом пересылать атрибуты диапазона при его переносе (а функции диапазона постоянно переносят свои аргументы в новые диапазоны), и Вы даже не можете должным образом защитить свою функцию от компиляции с типами, которые не будут работать с ней. И, конечно же, результаты static if и ограничения шаблона также влияют на самоанализ типа (поскольку они влияют на то, что будет и не будет компилироваться), поэтому эти три функции очень взаимосвязаны.

Действительно, основными причинами того, что диапазоны не очень хорошо работают в C ++, являются некоторые причины, по которым метапрограммирование в C ++ примитивно по сравнению с метапрограммированием в D. AFAIK, нет причины, по которой эти функции (или аналогичные) нельзя было добавить в C ++ и устранить проблему, но до тех пор, пока в C ++ не появятся возможности метапрограммирования, аналогичные D, диапазоны в C ++ будут серьезно нарушены.

Другие функции, такие как миксины и унифицированный синтаксис вызова функций, также могут помочь, но они далеко не так фундаментальны. Миксины могут помочь в первую очередь в уменьшении дублирования кода, а UFCS помогает в первую очередь в том, чтобы сделать так, чтобы универсальный код мог просто вызывать все функции, как если бы они были функциями-членами, чтобы, если случается, что тип определяет конкретную функцию (например, find) тогда это будет использоваться вместо более общей, свободной версии функции (и код все еще работает, если такая функция-член не объявлена, потому что тогда используется свободная функция). UFCS не является принципиально необходимым, и вы могли бы пойти в противоположном направлении и отдать предпочтение бесплатным функциям для всего (как C ++ 11 сделал с begin а также end), хотя для того, чтобы сделать это хорошо, требуется, чтобы свободные функции могли проверять существование функции-члена, а затем вызывать функцию-член внутри, а не использовать свои собственные реализации. Итак, снова вам нужно самоанализ типа вместе с static if и / или шаблонные ограничения.

Как бы я ни любил диапазоны, на данный момент я почти отказался от попыток что-либо с ними сделать в C ++, потому что функций, которые делают их разумными, просто нет. Но если другие люди могут понять, как это сделать, тем больше у них власти. Независимо от диапазонов, я бы хотел увидеть такие функции усиления C ++, как ограничения шаблона, static ifи введите интроспекцию, потому что без них метапрограммирование путь менее приятный, в том смысле, что, хотя я делаю это все время в D, я почти никогда не делаю это в C ++.

8