Всё началось с того, что мне понадобилось написать функцию, принимающую на себя владение произвольным объектом. Казалось бы, что может быть проще:
template <typename T>
void f (T t)
{
// Завладели экземпляром `t` типа `T`.
...
// Хочешь — переноси.
g(std::move(t));
// Не хочешь — не переноси.
...
}
Но есть один нюанс: требуется, чтобы принимаемый объект был строго rvalue
. Следовательно, нужно:
- Сообщать об ошибке компиляции при попытке передать
lvalue
. - Избежать лишнего вызова конструктора при создании объекта на стеке.
А вот это уже сложнее сделать.
Поясню.
Требования к входным аргументам
Допустим, мы хотим обратного, то есть чтобы функция принимала только lvalue
и не компилировалась, если ей на вход подаётся rvalue
. Для этого в языке присутствует специальный синтаксис:
template <typename T>
void f (T & t);
Такая запись означает, что функция f
принимает lvalue
-ссылку на объект типа T
. При этом заранее не оговариваются cv
-квалификаторы. Это может быть и ссылка на константу, и ссылка на неконстанту, и любые другие варианты.
Но ссылкой на rvalue
она быть не может: если передать в функцию f
ссылку на rvalue
, то программа не скомпилируется:
template <typename T>
void f (T &) {}
int main ()
{
auto x = 1;
f(x); // Всё хорошо, T = int.
const auto y = 2;
f(y); // Всё хорошо, T = const int.
f(6.1); // Ошибка компиляции.
}
Может, есть синтаксис и для обратного случая, когда нужно принимать только rvalue
и сообщать об ошибке при передаче lvalue
?
К сожалению, нет.
Единственная возможность принять rvalue
-ссылку на произвольный объект — это сквозная ссылка (forwarding reference
):
template <typename T>
void f (T && t);
Но сквозная ссылка может быть ссылкой как на rvalue
, так и на lvalue
. Следовательно, нужного эффекта мы пока не добились.
Добиться нужного эффекта можно при помощи механизма SFINAE
, но он достаточно громоздкий и неудобный как для написания, так и для чтения:
#include <type_traits>
template <typename T,
typename = std::enable_if_t<std::is_rvalue_reference<T &&>::value>>
void f (T &&) {}
int main ()
{
auto x = 1;
f(x); // Ошибка компиляции.
f(std::move(x)); // Всё хорошо.
f(6.1); // Всё хорошо.
}
А чего бы на самом деле хотелось?
Хотелось бы вот такой записи:
template <typename T>
void f (rvalue<T> t);
Думаю, смысл данной записи выражен достаточно чётко: принять произвольное rvalue
.
Первая мысль, которая приходит в голову, — это создать псевдоним типа:
template <typename T>
using rvalue = T &&;
Но такая штука, к несчастью, не сработает, потому что подстановка псевдонима происходит до вывода типа шаблона, поэтому в данной ситуации запись rvalue<T>
в аргументах функции полностью эквивалентна записи T &&
.
Забавно, что из-за ошибки в системе вывода типов компилятора Clang (версию точно не помню, кажется, 3.6) этот вариант "сработал". В компиляторе GCC этой ошибки не было, поэтому поначалу мой затуманенный безумной идеей разум решил, что ошибка не в Кланге, а в Гэцэцэ. Но, проведя, небольшое расследование, я понял, что это не так. А через некоторое время и в Кланге эту ошибку исправили.
Ещё одна идея — по сути, аналогичная, — которая может прийти в голову знатоку шаблонного метапрограммирования — это написать следующий код:
template <typename T>
struct rvalue_t
{
using type = T &&;
};
template <typename T>
using rvalue = typename rvalue_t<T>::type;
К структуре rvalue_t
можно было бы припилить SFINAE
, которое отваливалось бы, если бы T
было ссылкой на lvalue
.
Но, к сожалению, эта идея также обречена на провал, потому что такая структура "ломает" механизм вывода типов. В результате функцию f
вообще будет невозможно вызвать без явного указания аргумента шаблона.
Я очень расстроился и на время забросил эту идею.
Возвращение
В начале этого года, когда появилась новость о том, что комитет не включил концепты в стандарт C++17, я решил вернуться к заброшенной идее.
Немного поразмыслив, я сформулировал "требования":
- Должен работать механизм вывода типа.
- Должна быть возможность натравливать
SFINAE
-проверки на выводимый тип.
Из первого требования немедленно следует, что нужно всё-таки использовать псевдонимы типов.
Тогда возникает закономерный вопрос: можно ли натравливать SFINAE
на псевдонимы типов?
Оказывается, можно. И выглядеть это будет, например, следующим образом:
template <typename T,
typename = std::enable_if_t<std::is_rvalue_reference<T &&>::value>>
using rvalue = T &&;
Наконец-то получаем и требуемый интерфейс, и требуемое поведение:
template <typename T>
void f (rvalue<T>) {}
int main ()
{
auto x = 1;
f(x); // Ошибка компиляции.
f(std::move(x)); // Всё хорошо.
f(6.1); // Всё хорошо.
}
Победа.
Концепты
Внимательный читатель негодует: "Так где же тут концепты-то?".
Но если он не только внимательный, но ещё и сообразительный, то быстро поймёт, что эту идею можно использовать и для "концептов". Например, следующим образом:
template <typename I,
typename = std::enable_if_t<std::is_integral<I>::value>>
using Integer = I;
template <typename I>
void g (Integer<I> t);
Мы создали функцию, которая принимает только целочисленные аргументы. При этом получившийся синтаксис достаточно приятен и пишущему, и читающему.
int main ()
{
g(1); // Всё хорошо.
g(1.2); // Ошибка компиляции.
}
Что ещё можно сделать?
Можно попытаться ещё больше приблизиться к истинному синтаксису концептов, который должен выглядеть следующим образом:
template <Integer I>
void g (I n);
Для этого воспользуемся, кхм, макроснёй:
#define Integer(I) typename I, typename = Integer<I>
Получим возможность писать следующий код:
template <Integer(I)>
void g (I n);
На этом возможности данной техники, пожалуй, заканчиваются.
Недостатки
Если вспомнить название статьи, то можно подумать, что у этой техники есть какие-то недостатки.
Таки да. Есть.
Во-первых, она не позволяет организовать перегрузку по концептам.
Компилятор не увидит разницы между сигнатурами функций
template <typename I>
void g (Integer<I>) {}
template <typename I>
void g (Floating<I>) {}
и будет выдавать ошибку о переопределении функции g
.
Во-вторых, невозможно одновременно проверить несколько свойств одного типа. Вернее, возможно, но придётся городить достаточно сложные конструкции, которые сведут на нет всю удобочитаемость.
Выводы
Приведённая техника — назовём её техникой фильтрующего псевдонима типов — имеет достаточно ограниченную область применения.
Но в тех случаях, когда она применима, она открывает программисту достаточно неплохие возможности для чёткого выражения намерения в коде.
Считаю, что она имеет право на жизнь. Лично я пользуюсь. И не жалею.
Ссылки по теме
Комментарии (50)
bfDeveloper
05.07.2016 14:00+2Божественно. Особенно радует, что это не экран шаблонного кода, а вполне вменяемые 2 строки. Удачно отделяется концепт с условием от определения самого метода. Всё таки даже без концептов С++11 силён.
Долго соображал над конструкцией typename = std::enable_if_t<...>. Я правильно понял, что это просто безымянный аргумент шаблона для using? А то на первый взгляд выглядит как специальный синтаксис.AndreySu
05.07.2016 14:11+3да, и с паттерн матчингом который отключит компиляцию если тип не std::is_integral
izvolov
05.07.2016 14:20+2Я правильно понял, что это просто безымянный аргумент шаблона для using?
Совершенно верно.
Это безымянный аргумент шаблона со значением по-умолчанию.VioletGiraffe
05.07.2016 16:10Какой смысл в безымянных шаблонных параметрах? Зачем это вообще компилируется, почему не синтаксическая ошибка?
Overlordff
05.07.2016 16:33+5Смысл безымянного параметра в названии — нам не интересно его имя, мы его далее никак не используем. А сам параметр нужен для SFINAE
Unrul
05.07.2016 14:24+5Использовать перегрузку функций по концептам можно с помощью следующего способа:
template <typename T> auto foo(T i) -> enable_if_t<is_integral_v<T>, void> { printf("Int\n"); } template <typename T> auto foo(T i) -> enable_if_t<is_floating_point_v<T>, void> { printf("Float\n"); } template <typename T> auto foo(T i) -> enable_if_t<is_class_v<T>, void> { printf("Struct\n"); } int main() { foo(1); foo(1u); foo(1.2); foo(1.2f); foo(make_tuple(1)); return 0; }
Или так:
template <typename T, typename = enable_if_t<is_integral_v<T>>> using IsInteger = integral_constant<int, 0>; template <typename T, typename = enable_if_t<is_floating_point_v<T>>> using IsFloat = integral_constant<int, 1>; template <typename T, typename = enable_if_t<is_class_v<T>>> using IsStruct = integral_constant<int, 2>; template <typename T> auto foo(T i, IsInteger<T> = {}) -> void { printf("Int\n"); } template <typename T> auto foo(T i, IsFloat<T> = {}) -> void { printf("Float\n"); } template <typename T> auto foo(T i, IsStruct<T> = {}) -> void { printf("Struct\n"); }
Не так элегантно, но тем не менее. Будем надеяться, что концепты всё таки включат в стандарт когда-нибудь.izvolov
05.07.2016 14:40Если хочется более полноценной работы с концептами, то нужно смотреть в сторону имеющихся библиотек (см. ссылки в конце статьи).
Смысл приведённой техники именно в простоте и удобочитаемости.
DistortNeo
05.07.2016 16:34+2Лучше не использовать продвинутые заменители концептов до их появления в стандарте.
Причины простые:
1. Крайне неинформативный вывод сообщений об ошибках в шаблонах.
2. Замедление скорости компиляции.
3. Не всегда очевидна логика работы шаблонных конструкций без вдумчивого анализа кода.
4. В конце концов, монструозные конструкции ломают IDE, в результате чего IDE превращается просто в редактор с подсветкой синтаксиса.
Указанный в публикации пример — исключение, подтверждающее правило.
P.S. Я бы просто воткнул в функцию static_assert.Unrul
05.07.2016 18:58+1, static_assert будет наиболее предпочтителен, если не нужно использовать перегрузку.
monah_tuk
06.07.2016 12:23+2Замедление скорости компиляции.
не думаю, что концепты сильно её ускорят, скорее наоборот. Ну т.е. код с одинаковым набором ограничений при помощи концептов и SFINAE думаю будет компилироваться со сравнительной скоростью. Нужно попробовать сравнить: в gcc-6 концепты завезли.
Shamov
05.07.2016 16:42+1Синтаксис настолько близок к настоящим концептам, что можно попробовать сделать универсальные макросы, которые будут использовать эту технику при отсутствии поддержки концептов в компиляторе. Если это удастся, то можно будет писать такой код, который будет компилиться как на нормальном компиляторе, так и на «concept-enabled» gcc.
ulysses
05.07.2016 17:20Поздравляю, вы переизобрели кусочек Concepts Lite! Которые, как вы упомянули, надысь выбросили из C++17. Вы можете сравнить свою логику и логику Саттона (Andrew Sutton), если включите какой-нибудь его доклад на Ютубе. Идея та же: CL это лёгкая обёртка над
enable_if
. Проблем несколько. Первая порция, которая касается вашего решения и не касается его реализации на уровне компилятора: это, как тоже сказали выше, время компиляции и, более важно, информативность ошибок (особенно в вашей версии с макросом она будет жуткой, думаю). Саттон специально отмечает, что на уровне компилятора это лучше решать.
Про перегрузку вы сами написали. Есть ещё проблема, которая касается и вашего решения, и CL: нет проверки ограничений внутри тела шаблона. То есть вы можете выставить какой-то концепт-интерфейс, но внутри шаблона использовать больше, чем затребовали в этом интерфейсе (по недосмотру, который не так уж нереален в таком тяжёлом и синтаксически и семантически языке как C++). Конечно, ошибка будет на стадии компиляции. Но всё равно досадно. Особенно если это библиотека и на неё напорется какой-то неискушённый пользователь этой библиотеки.
izvolov
05.07.2016 17:53+2Я не понял вашей мысли.
Вы упрекаете меня в том, что я не решаю проблему на этапе компиляции?
К сожалению, я пока не настолько крут, чтобы писать компиляторы или заседать в комитете по стандартизации.
Я просто сымитировл часть функциональности, которая (и я об этом открыто говорю в статье) не даёт никаких преимуществ над другими методами пред-C++17, за исключением удобочитаемости.
ulysses
05.07.2016 22:49Я ни в чём никого не упрекаю. Моя мысль такая, что прослушав пару докладов Саттона одно или двухгодичной давности можно было бы написать вариант с определением
rvalue
черезenable_if
+type_traits
довольно быстро. Потому что там это всё разжёвывается хорошо. Ну и компилятор писать я вас ни в коем случае не призываю. Просто пишу о пользе просмотра докладов ведущих специалистов.
DmitryBabokin
05.07.2016 17:27+5А почему не объявить вот так?
void g(T &t) = delete; void g(T &&t);
Тогда lvalue замапится на первый вариант и выдаст ошибку компиляции.izvolov
05.07.2016 18:10Преимущества записи
rvalue<T>
лично я вижу следующие:
Она явно выражает намерение автора.
Удаление варианта с
lvalue
-ссылкой, в каком-то смысле, тоже явно выражает намерение. Но тогда читателю перегрузки со сквозной ссылкой нужно помнить, что сквозная ссылка на самом деле не может бытьlvalue
-ссылкой, а перегрузка сlvalue
-ссылкой явно удалена.
Функция становится самодостаточной.
Если в вашем варианте случайно удалить или закомментировать вариант с
lvalue
-ссылкой, изменится поведение варианта со сквозной ссылкой. При этом программа, вероятно, продолжит компилироваться и работать.
У меня такой проблемы не возникнет.
DmitryBabokin
05.07.2016 18:18Согласен. Но как решение для мира, где нет концептов, должно вполне годиться.
bfDeveloper
05.07.2016 18:26с lvalue gcc говорит, что
error: call of overloaded 'bar(int&)' is ambiguous
bar(a);
^
Потому что обе сигнатуры — одно и то же для перегрузок. То есть всё работает, но ошибка совсем не информативна.
P.S. Лучше воспользоваться не =delete, а static_assert (false, «lvalue is not supported»);EvgK
11.11.2016 22:14В таких экспериментах обычно как раз и генерируют по одному фотону, потому что сопоставление сигнального и соответвующего ему холостого ведется на основе времени (сигнальный испустили во время Х, холостой детектировали в Х + А\с (где А — длина пути до детектора). Наложение картин от разных групп фотонов обойти действительно никак нельзя — информацию о том какой фотон принадлежит какой группе мы получаем только после детектирования холостого партнера.
DmitryBabokin
05.07.2016 19:12В том-то и дело, что не одно и тоже, в этом и фишка.
void g(int &i) = delete; void g(int &&i); void foo(int i) { g(i); g(5); }
clang 3.8:
> clang++ -c foo.cpp -std=c++11
foo.cpp:5:3: error: call to deleted function 'g'
g(i);
^
foo.cpp:1:6: note: candidate function has been explicitly deleted
void g(int &i) = delete;
^
foo.cpp:2:6: note: candidate function not viable: no known conversion from 'int' to 'int &&' for 1st argument
void g(int &&i);
^
1 error generated.
gcc 5.3.1:
> g++ -c foo.cpp -std=c++11
foo.cpp: In function ‘void foo(int)’:
foo.cpp:5:6: error: use of deleted function ‘void g(int&)’
g(i);
^
foo.cpp:1:6: note: declared here
void g(int &i) = delete;
^
0xd34df00d
09.07.2016 22:43+2
horo
05.07.2016 18:11+1Когда вы в статье упомянули о том, что нужно сообщать об ошибке компиляции, мне сразу подумалось, что проблему можно решить через static_asser, как уже предлагали выше. Например вот так:
static_assert(std::is_rvalue_reference <T &&>::value,"Message here");
izvolov
05.07.2016 18:28+3Явное лучше неявного.
Когда я пишу
rvalue<T>
, я сообщаю читателю моего кода, что у меня здесь на входе всегдаrvalue
.
Если же я пишу обычную сквозную ссылку иstatic_assert
внутри функции, то читателю сложнее понять моё намерение. Потому что ему ещё нужно дочитать до этой проверки. А потом ему это нужно постоянно помнить, чтобы случайно не воткнутьforward
вместоmove
.
Так же, как и использование стандартных алгоритмов вместо циклов: мы сообщаем читателю что мы хотели сделать, а не как мы это сделали.
См. также комментарий выше.
XoJIoD
11.11.2016 23:32+1Ну, раз уж речь зашла, то позвольте еще вопрос) В эксперименте с ластиком, если мы удалим маркеры со всех холостых фотонов, но после того, как сигнальные дошли до экрана, эти сигнальные ведь также будут показывать отсутствие интерференции?
EvgK
11.11.2016 23:54Я не физик, поэтому могу ошибаться. Но насколько я понимаю, в эксперименте с ластиком у нас 4 детектора: D1, D2, D3, D4. Если фотон попал в D1 или D2 — информация о пути «стерта». Если мы удалим маркеры со всех фотонов, они все попадут в D1 или D2. Но важный момент в том, что мы получим ДВЕ интерференционные картины (для фотонов из D1 и D2 соответственно), причем пики одной совпадают с впадинами другой. Соответственно эти две картины в сумме дают обычное хаотическое распределение (которое мы и видим на экране). Вот тут: https://en.wikipedia.org/wiki/Delayed_choice_quantum_eraser есть картинка, показывающая как выглядят результаты (Figure 4). Видно, что если сложить R01 и R02 — получим хаотический набор точек.
DarkEld3r
06.07.2016 12:42+1А разве на расте эта задача вообще решается?
Да, из-за необходимости указывать трейты для параметров дженериков получается, что некий (обязательный) аналог концептов уже есть. И с передачей владения тоже всё неплохо сделано. Но если представить, что мы хотим решить задачу так как она сформулирована в статье, то что можем сделать?
0xd34df00d
09.07.2016 22:49Невольно сравнивая с теми же вещами в Haskell, не могу не почувствовать разницу в возможностях. Серьёзно, в ряде важных случаев на тайпклассах отрицание не выражается (вроде «реализовать тайпкласс для всех вещей, не реализующих тайпкласс Bounded»), по крайней мере, жутчайших костылей, после которых тайпчекер сходит с ума.
VoidEx
10.07.2016 21:45реализовать тайпкласс для всех вещей, не реализующих тайпкласс Bounded
А это как? Создал тип данных, импортнул ваш модуль — и инстанс тайпкласса есть, могу им воспользоваться в функции. Добавил в соседнем инстанс Bounded — и в нём инстанса вашего тайпкласса уже нет, но функцию мою, к примеру, можно звать?0xd34df00d
11.07.2016 04:48Добавил в соседнем инстанс Bounded — и в нём инстанса вашего тайпкласса уже нет, но функцию мою, к примеру, можно звать?
Эм, почему? У вашей функции будет же сигнатура вроде¬(Bounded a) => a -> foobar
, разве нет?
Правда, лично мне это надо было в какой-то другой задаче и локально на уровне модуля. Там даже всякие OverlappingInstances и UndecidableInstances не помогали совсем. Ну, и немудрено, на самом деле.VoidEx
11.07.2016 10:12У вашей функции будет же сигнатура вроде ¬(Bounded a) => a -> foobar, разве нет?
Необязательно, может, простоMyType -> Blah
.
Это просто выглядит как-то императивно что ли, когда от порядка объявления зависит код. Грубо говоря, если запретить даже orphans, то всё будет достаточно строго — появился класс и вместе с ним все инстансы, появился тип — с ним все инстансы. Всё достаточно однозначно. Необходимость в orphan instances ещё можно понять, если авторы и класса, и типа, — сторонние люди, а инстанс вполне однозначен; а вот описанная ситуация, когда от добавления инстанса другой инстанс должен пропадать, создаёт устойчивое впечатление, что что-то тут не так :)0xd34df00d
12.07.2016 03:38Необязательно, может, просто MyType -> Blah
А какое отношение тогдаMyType
имеет кBounded
? Я сходу не могу придумать, как оно у вас бы так получилось.
Либо там всё совсем скрыто, и от наличия инстанса в текущем модуле ничего не сломается.
То есть, я не вижу, где оно бы от порядка зависело.VoidEx
13.07.2016 17:59Как в изначальном сценарии:
А это как? Создал тип данных [
MyType
], импортнул ваш модуль — и инстанс [ваш, для всех, у кого нет инстансаBounded
] тайпкласса есть, могу им воспользоваться в функции [foo :: MyType -> Blah
]. Добавил в соседнем инстанс Bounded — и в нём инстанса вашего тайпкласса уже нет, но функцию мою [foo
], к примеру, можно звать?
А если я выставлю набор функций, каждая из которых просто напросто дублирует функции вашего класса, но специализирована дляMyType
и соответственно реализована через позыв функций вашего have-no-Bounded-инстанса, то получится как бы обходной манёвр, несмотря на наличие в текущем scope инстансаBounded
дляMyType
, я буду использовать proxy-функции из соседнего модуля, где этогоBounded
нету.
Я правильно понял, что ваш инстанс не сработает для типа, у которого в scope есть также инстансBounded
? Т.е. если другого инстанса нет, будет ошибка?
Сравните, например, с Overlapped. Там есть один инстанс, его можно подменить другим (более специализированным), но, вроде как, нельзя убрать инстанс.
Я не говорю, что с этим будут какие-то проблемы (хотя, наверное, могут и быть, но я не буду ручаться за конкретные сценарии), я о том, что это как-то нематематично что ли, как и Overlapped/Undecidable, в общем-то.0xd34df00d
14.07.2016 05:37А, я вас понял, наконец!
Функцию звать, получается, можно, да. Это всё ведь детали реализации. Более того, если я правильно вас понимаю, вместо костылей с дублированием API вы можете просто взять и написать newtype.
На самом деле в моей задаче Overlapped не подходил, хотя инстанс был и для Bounded-варианта. Я, к сожалению, напрочь забыл подробности, но мне хотелось тайпклассLowerBound a
с методомlower :: a
, который был быminBound
для Bounded и -\infty для Real. Не помню, почему у меня Overlapped не хватало, и откуда вылезала необходимость в отрицании тайпкласса :(
tower120
06.07.2016 22:31В gcc 6.1 концепты уже есть из TS спецификации.
Нужно добавить следующие флаги для компиляции:
-fconcepts -std=c++1z
Пример http://coliru.stacked-crooked.com/a/a59567cac9c7681d
Документация http://en.cppreference.com/w/cpp/language/constraintsizvolov
06.07.2016 22:53+1Есть. Но необходимость в костылях на велосипедном приводе полностью отпадёт только тогда, когда это будет во всех компиляторах, и во всех них оно будет работать одинаково и включаться одними флагами компиляции.
Всё-таки TS и стандарт — разные вещи.
tower120
07.07.2016 01:13-1Зачем вам во всех компиляторах? Вы же на каком то конкретном работаете, а не на всех сразу.
izvolov
07.07.2016 01:21+2Странный вопрос.
Во-первых, допустим, я хочу писать переносимый код.
Во-вторых, допустим, я хочу в бою использовать GCC, а для статического анализа — Clang.
В-третьих, чем больше компиляторов с выкрученными на максимум предупреждениями скомпилируют код, тем больше я буду в нём уверен.
Ну и т.д.
0xd34df00d
09.07.2016 22:51Переносимый код для меня — это когда код не привязан к нестандартным особенностям конкретного компилятора.
Ну, формально переносимый код может даже ничем не собираться, и у меня был опыт написания такого кода, потому что в gcc одни баги, в clang — другие :)
А в бою, как по мне, clang уже генерит более оптимизированный код. На днях с автовекторизатором баловался — в моей задаче clang смог больше вещей раскрутить и более оптимизированный ассемблер выдать. Но это так, конечно же, YMMV.izvolov
09.07.2016 23:52Переносимый код для меня — это когда код не привязан к нестандартным особенностям конкретного компилятора.
Ну так код из публикации и не привязан ни к каким нестандартным особенностям.
clang уже генерит более оптимизированный код
В моих проектах гэцэцэ побыстрее.
Хотя на простых испытательных примерах кланг действительно частенько генерирует более оптимальный ассемблерный код.
YMMV
Чё?
0xd34df00d
10.07.2016 02:17Ну так код из публикации и не привязан ни к каким нестандартным особенностям.
Да, но
Зачем вам во всех компиляторах? Вы же на каком то конкретном работаете, а не на всех сразу.
Если этот компилятор поддерживает стандарт на должном уровне, то и хорошо, разве нет?
Хотя на простых испытательных примерах кланг действительно частенько генерирует более оптимальный ассемблерный код.
Ну, не сказал бы, что у меня простые испытательные примеры. Циклы не самые тривиальные, дешугаринг сделать надо не самый тривиальный от всех этих итераторов, границы циклов известны не всегда, операции доступа памяти скрыты за operator() и шаблонными стратегиями лейаута матриц в памяти…
Я был приятно удивлён.
Чё?
Your mileage may vary.
Sharkow
Зачем нужен запрет передавать lvalue? Как архитектурное решение — кажется неудачным.
izvolov
Подчеркнуть, что у входного объекта нет других владельцев.
Почему?