Концепты для отчаявшихся
Всё началось с того, что мне понадобилось написать функцию, принимающую на себя владение произвольным объектом. Казалось бы, что может быть проще:
template
void f (T t)
{
// Завладели экземпляром `t` типа `T`.
...
// Хочешь — переноси.
g(std::move(t));
// Не хочешь — не переноси.
...
}
Но есть один нюанс: требуется, чтобы принимаемый объект был строго rvalue
. Следовательно, нужно:
- Сообщать об ошибке компиляции при попытке передать
lvalue
. - Избежать лишнего вызова конструктора при создании объекта на стеке.
А вот это уже сложнее сделать.
Поясню.
Требования к входным аргументам
Допустим, мы хотим обратного, то есть чтобы функция принимала только lvalue
и не компилировалась, если ей на вход подаётся rvalue
. Для этого в языке присутствует специальный синтаксис:
template
void f (T & t);
Такая запись означает, что функция f
принимает lvalue
-ссылку на объект типа T
. При этом заранее не оговариваются cv
-квалификаторы. Это может быть и ссылка на константу, и ссылка на неконстанту, и любые другие варианты.
Но ссылкой на rvalue
она быть не может: если передать в функцию f
ссылку на rvalue
, то программа не скомпилируется:
template
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
void f (T && t);
Но сквозная ссылка может быть ссылкой как на rvalue
, так и на lvalue
. Следовательно, нужного эффекта мы пока не добились.
Добиться нужного эффекта можно при помощи механизма SFINAE
, но он достаточно громоздкий и неудобный как для написания, так и для чтения:
#include
template ::value>>
void f (T &&) {}
int main ()
{
auto x = 1;
f(x); // Ошибка компиляции.
f(std::move(x)); // Всё хорошо.
f(6.1); // Всё хорошо.
}
А чего бы на самом деле хотелось?
Хотелось бы вот такой записи:
template
void f (rvalue t);
Думаю, смысл данной записи выражен достаточно чётко: принять произвольное rvalue
.
Первая мысль, которая приходит в голову, — это создать псевдоним типа:
template
using rvalue = T &&;
Но такая штука, к несчастью, не сработает, потому что подстановка псевдонима происходит до вывода типа шаблона, поэтому в данной ситуации запись rvalue
в аргументах функции полностью эквивалентна записи T &&
.
Забавно, что из-за ошибки в системе вывода типов компилятора Clang (версию точно не помню, кажется, 3.6) этот вариант «сработал». В компиляторе GCC этой ошибки не было, поэтому поначалу мой затуманенный безумной идеей разум решил, что ошибка не в Кланге, а в Гэцэцэ. Но, проведя, небольшое расследование, я понял, что это не так. А через некоторое время и в Кланге эту ошибку исправили.
Ещё одна идея — по сути, аналогичная, — которая может прийти в голову знатоку шаблонного метапрограммирования — это написать следующий код:
template
struct rvalue_t
{
using type = T &&;
};
template
using rvalue = typename rvalue_t::type;
К структуре rvalue_t
можно было бы припилить SFINAE
, которое отваливалось бы, если бы T
было ссылкой на lvalue
.
Но, к сожалению, эта идея также обречена на провал, потому что такая структура «ломает» механизм вывода типов. В результате функцию f
вообще будет невозможно вызвать без явного указания аргумента шаблона.
Я очень расстроился и на время забросил эту идею.
Возвращение
В начале этого года, когда появилась новость о том, что комитет не включил концепты в стандарт C++17, я решил вернуться к заброшенной идее.
Немного поразмыслив, я сформулировал «требования»:
- Должен работать механизм вывода типа.
- Должна быть возможность натравливать
SFINAE
-проверки на выводимый тип.
Из первого требования немедленно следует, что нужно всё-таки использовать псевдонимы типов.
Тогда возникает закономерный вопрос: можно ли натравливать SFINAE
на псевдонимы типов?
Оказывается, можно. И выглядеть это будет, например, следующим образом:
template ::value>>
using rvalue = T &&;
Наконец-то получаем и требуемый интерфейс, и требуемое поведение:
template
void f (rvalue) {}
int main ()
{
auto x = 1;
f(x); // Ошибка компиляции.
f(std::move(x)); // Всё хорошо.
f(6.1); // Всё хорошо.
}
Победа.
Концепты
Внимательный читатель негодует: «Так где же тут концепты-то?».
Но если он не только внимательный, но ещё и сообразительный, то быстро поймёт, что эту идею можно использовать и для «концептов». Например, следующим образом:
template ::value>>
using Integer = I;
template
void g (Integer t);
Мы создали функцию, которая принимает только целочисленные аргументы. При этом получившийся синтаксис достаточно приятен и пишущему, и читающему.
int main ()
{
g(1); // Всё хорошо.
g(1.2); // Ошибка компиляции.
}
Что ещё можно сделать?
Можно попытаться ещё больше приблизиться к истинному синтаксису концептов, который должен выглядеть следующим образом:
template
void g (I n);
Для этого воспользуемся, кхм, макроснёй:
#define Integer(I) typename I, typename = Integer
Получим возможность писать следующий код:
template
void g (I n);
На этом возможности данной техники, пожалуй, заканчиваются.
Недостатки
Если вспомнить название статьи, то можно подумать, что у этой техники есть какие-то недостатки.
Таки да. Есть.
Во-первых, она не позволяет организовать перегрузку по концептам.
Компилятор не увидит разницы между сигнатурами функций
template
void g (Integer) {}
template
void g (Floating) {}
и будет выдавать ошибку о переопределении функции g
.
Во-вторых, невозможно одновременно проверить несколько свойств одного типа. Вернее, возможно, но придётся городить достаточно сложные конструкции, которые сведут на нет всю удобочитаемость.
Выводы
Приведённая техника — назовём её техникой фильтрующего псевдонима типов — имеет достаточно ограниченную область применения.
Но в тех случаях, когда она применима, она открывает программисту достаточно неплохие возможности для чёткого выражения намерения в коде.
Считаю, что она имеет право на жизнь. Лично я пользуюсь. И не жалею.
Ссылки по теме
- Библиотека «Boost Concept Check»
- Концепты из прототипа библиотеки диапазонов «range-v3»
- Библиотека «TICK»
- Статья «Concepts Without Concepts»