SFINAE для функции тела - PullRequest
       8

SFINAE для функции тела

0 голосов
/ 23 ноября 2018

Я часто использую SFINAE для удаления функции из набора перегрузки, если тело функции не имеет смысла (т.е. не компилируется).Можно ли добавить в C ++ простой оператор require?

Например, давайте возьмем функцию:

template <typename T>
T twice(T t) {
  return 2 * t;
}

Тогда я получу:

twice(1.0);
twice("hello");  // Error: invalid operands of types ‘int’ and ‘const char*’ to binary ‘operator*’

Я хочу получить ошибку, которая говорит, что не существует функции twice для аргумента типа const char *

Я хотел бы написать что-то вроде:

template <typename T>
requires function_body_compiles
T twice(T t) {
  return 2 * t;
}

Тогда я быget

twice(1.0);
twice("hello");  // Error: no matching function for call to ‘twice2(const char [6])’

Больше мотивации: Я наблюдал за выступлением Семантика Кошмара Движения для Тривиальных Классов , и его последний SFINAE в основном говорит: используйтеэтот конструктор, когда он компилируется.Для более сложного конструктора написание правильного SFINAE было бы кошмаром.

Как вы думаете, имеет ли смысл добавлять requires function_body_compiles в c ++?Или мне не хватает фундаментальной проблемы?Насколько сильно это может быть использовано неправильно или неправильно?

Ответы [ 3 ]

0 голосов
/ 23 ноября 2018

Вы можете в некоторой степени делать то, что вы хотите, с помощью require-выражения (https://godbolt.org/z/6FDT45):

template <typename T> requires requires(T t) { { 2 * t } -> T; }
T twice(T t) {
  return 2 * t;
}

int main()
{
twice(1.0);
twice("hello"); // Error: Constraints not satisfied
}

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

  • Они не ограничены типами возвращаемых данных.
  • Может быть столько выражений, сколько необходимо.

То, что вы хотели бы получить, называется «проверкой определения концепции».Бьярн Страуструп обсуждает, почему он отсутствует в концептуальном проекте в статье P0557R0 (раздел 8.2).

0 голосов
/ 23 ноября 2018

Самая большая причина, по которой у нас нет этой функции, заключается в том, что она сложная.

Это сложно, потому что для этого требуется, чтобы компиляторы могли компилировать почти произвольный код C ++, получать ошибки, а затем возвращаться обратно чисто.

Существующие компиляторы C ++, где не все предназначены для этого.На самом деле MSVC потребовалось большую часть десятилетия, чтобы иметь разумно совместимую поддержку decltype SFINAE.

Для полнофункциональных тел сделать это было бы еще сложнее.


Теперь, даже еслиэто было легко, есть причины не делать этого.Он смешивает реализацию и интерфейс довольно ужасным образом.

Вместо того, чтобы идти по этому пути, комитет C ++ движется в совершенно ином направлении.

Концепции - это идея, которую вы можете выразить в требованиях кпечатает разумными, обычно именуемыми способами.Они приходят в .

Как уже упоминалось в другом ответе,

template <typename T> requires requires(T t) { { 2 * t } -> T; }
T twice(T t) {
  return 2 * t;
}

- это способ сделать это, но этот способ считается дурным тоном.Вместо этого вы должны написать концепцию «можно умножить на целое число и получить обратно тот же тип».

template<typename T>
concept IntegerScalable = requires(T t) {
  { 2 * t } -> T;
};

тогда мы можем

template <IntegerScalable T>
T twice(T t) {
  return 2 * t;
}

и все готово.

Требуемый следующий шаг называется «проверенные концепции».В проверенных концепциях концепт преобразовывается в набор интерфейсов времени компиляции для вашего типа T.

. Затем проверяется тело функции, чтобы убедиться, что ничего не сделано с типом T, которыйне является требованием концепции.

Используя теоретически проверенную концепцию будущего,

template <IntegerScalable T>
T twice(T t) {
  T n = 7;
  if (n > t) return n;
  return 2 * t;
}

это будет отклонено компилятором при компиляции шаблона даже раньшевызов шаблона был выполнен, потому что концепция IntegerScalable не гарантировала, что вы можете либо инициализировать T целым числом, либо сравнивать один T с другим с >.Кроме того, я думаю, что для вышеизложенного требуется конструкция move.


Сегодня вы можете сделать хак.

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

тогда ваш код можно записать в виде:

template<class T>
T twice(T t)
RETURNS( 2 * t )

и вы получите SFINAE дружественную версию twice.Это также будет настолько же исключительным, насколько это возможно.

Вариант использования с использованием => для замены RETURNS и некоторых других вещей был предложен @ Barry , но это былогод с тех пор, как я видел его движение.

Тем временем RETURNS выполняет большую часть тяжелой работы.

0 голосов
/ 23 ноября 2018

Барри Ревзин подал [предложение] именно за то, что вы просите, но в контексте лямбда-выражений.Поскольку это требует построения лямбды, синтаксис будет немного другим:

auto twice = [](auto t) => 2 * t; //sfinae friendly

или даже:

auto twice = 2 * $0;

Тем не менее, статус этого предложения все еще остается неопределенным.Вы можете проверить это [здесь] .

Однако в случае конструктора, я не уверен, будет ли способ применить такую ​​конструкцию, даже еслипредложение будет принято.Тем не менее, если кто-то увидел необходимость в лямбда-выражениях, то в общем случае возможно развитие языка.

...