- PVSM.RU - https://www.pvsm.ru -
Всё началось с того, что мне понадобилось написать функцию, принимающую на себя владение произвольным объектом. Казалось бы, что может быть проще:
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 [1], я решил вернуться к заброшенной идее.
Немного поразмыслив, я сформулировал "требования":
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.
Во-вторых, невозможно одновременно проверить несколько свойств одного типа. Вернее, возможно, но придётся городить достаточно сложные конструкции, которые сведут на нет всю удобочитаемость.
Приведённая техника — назовём её техникой фильтрующего псевдонима типов — имеет достаточно ограниченную область применения.
Но в тех случаях, когда она применима, она открывает программисту достаточно неплохие возможности для чёткого выражения намерения в коде.
Считаю, что она имеет право на жизнь. Лично я пользуюсь [2]. И не жалею.
Автор: izvolov
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/150904
Ссылки в тексте:
[1] комитет не включил концепты в стандарт C++17: https://www.youtube.com/watch?v=KJIh86pmP_Y
[2] пользуюсь: https://github.com/izvolov/burst/blob/master/burst/rvalue.hpp
[3] Библиотека "Boost Concept Check": http://www.boost.org/doc/libs/1_61_0/libs/concept_check/concept_check.htm
[4] Концепты из прототипа библиотеки диапазонов "range-v3": https://github.com/ericniebler/range-v3/blob/master/include/range/v3/utility/concepts.hpp
[5] Библиотека "TICK": http://pfultz2.github.io/Tick/doc/html/
[6] Статья "Concepts Without Concepts": https://akrzemi1.wordpress.com/2016/03/21/concepts-without-concepts/
[7] Источник: https://habrahabr.ru/post/304728/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.