Концепти для зневірених

Все почалося з того, що мені знадобилося написати функцію, що приймає на себе володіння довільним об'єктом. Здавалося б, що може бути простіше:
template < typename T>
void f (T t)
{
// Заволоділи екземпляром `t` типу `T`.
...

// Хочеш — терпи.
g(std::move(t));

// Не хочеш — не перенось.
...
}

Але є один нюанс: потрібно, щоб приймається об'єкт був суворо
rvalue
. Отже, потрібно:
  1. повідомити про помилку компіляції при спробі передати
    lvalue
    .
  2. Уникнути зайвого виклику конструктора при створенні об'єкта на стеку.
А ось це вже складніше зробити.
Поясню.
Вимоги до вхідних аргументів
Припустимо, ми хочемо зворотного, тобто щоб функція приймала тільки
lvalue
та не компилировалась, якщо їй на вхід подається
rvalue
. Для цього в мові присутній спеціальний синтаксис:
template < typename T>
void f (T & t);

Такий запис означає, що функція
f
бере
lvalue
-посилання на об'єкт типу
T
. При цьому заздалегідь не обумовлюються
m
-квалификаторы. Це може бути і посилання на константу, і посилання на неконстанту, і будь-які інші варіанти.
Але посиланням на
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. Повинен працювати механізм виведення типу.
  2. Повинна бути можливість нацьковувати
    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
.
По-друге, неможливо одночасно перевірити кілька властивостей одного типу. Вірніше, можливо, але доведеться створювати досить складні конструкції, які зведуть нанівець усю читабельність.
Висновки
Наведена техніка — назвемо її технікою фільтруючого псевдоніма типів — має досить обмежену область застосування.
Але в тих випадках, коли вона застосовна, вона відкриває програмісту досить непогані можливості для чіткого вираження наміру в коді.
Вважаю, що вона має право на життя. Особисто я користуюся. І не шкодую.
Посилання по темі
  1. Бібліотека "Boost Concept Check"
  2. Концепти з прототипу бібліотеки діапазонів "range-v3"
  3. Бібліотека "TICK"
  4. Стаття "Concepts Without Concepts"
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.