Что такое не выводимый контекст?

Недавно я столкнулся с этим вопросом, и ответы на него можно суммировать как "Это не выводимый контекст".

В частности, первый говорит, что это такая вещь, а затем перенаправляет на стандарт для "деталей", а второй цитирует стандарт, который, по меньшей мере, загадочен.

Может ли кто-нибудь объяснить простым смертным, таким как я, что такое недооцененный контекст, когда это происходит и почему это происходит?

2 ответа

Решение

Удержание относится к процессу определения типа параметра шаблона из заданного аргумента. Это относится к шаблонам функций, autoи несколько других случаев (например, частичная специализация). Например, рассмотрим:

template <typename T> void f(std::vector<T>);

Теперь, если вы скажете f(x)где вы заявили std::vector<int> x;, затем T выводится как intи вы получите специализацию f<int>,

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

Тем не менее, существуют также не выводимые контексты, где вычет невозможен. Каноническим примером является "параметр шаблона, который появляется слева от :::

template <typename> struct Foo;

template <typename T> void g(typename Foo<T>::type);

В этом шаблоне функции T в списке параметров функции находится в не выводимом контексте. Таким образом, вы не можете сказать, g(x) и вывести T, Причина этого заключается в том, что не существует "обратной переписки" между произвольными типами и членами Foo<T>::type, Например, вы можете иметь специализации:

 template <> struct Foo<int>       { using type = double; };
 template <> struct Foo<char>      { using type = double; };
 template <> struct Foo<float>     { using type = bool; };
 template <> struct Foo<long>      { int type = 10; };
 template <> struct Foo<unsigned>  { };

Если вы позвоните g(double{}) Есть два возможных ответа Tи если вы позвоните g(int{}) нет ответа. В общем, нет никакой связи между параметрами шаблона класса и членами класса, поэтому вы не можете выполнить разумный вывод аргумента.


Иногда полезно явно запретить вывод аргументов. Это, например, случай для std::forward, Другой пример, когда у вас есть конверсии из Foo<U> в Foo<T>скажем, или другие преобразования (подумайте std::string а также char const *). Теперь предположим, что у вас есть бесплатная функция:

template <typename T> bool binary_function(Foo<T> lhs, Foo<T> rhs);

Если вы позвоните binary_function(t, u), то вычет может быть неоднозначным и, следовательно, потерпеть неудачу. Но разумно вывести только один аргумент, а не вывести другой, что позволяет неявные преобразования. Теперь необходим явно не выводимый контекст, например, такой:

template <typename T>
bool binary_function(Foo<T> lhs, typename std::common_type<Foo<T>>::type rhs)
{
    return binary_function(lhs, rhs);
}

(Возможно, у вас возникли такие проблемы удержания с чем-то вроде std::min(1U, 2L).)

Что такое невыведенный контекст?

Невыведенный контекст — это контекст, в котором аргументы шаблона не могут быть выведены. Не всегда возможно вывести параметры шаблона из какой-либо конструкции. Например:

      template <int N>
void foo(int x = N * N); // called like foo()

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

Список невыведенных контекстов c++20

Предположим, что в следующем списке

  • это некоторый параметр типа шаблона
  • это какой-то параметр шаблона, не относящийся к типу
      template <typename T, int N>

В каждом заголовке цитируется и поясняется один пункт в [temp.deduct.type] p5 .

1.Foo<T>::type

Спецификатор вложенного имени типа, указанного с использованием квалифицированного идентификатора .

Нет возможности сделать вывод, потому чтоtypeэто просто псевдоним, напримерstd::vector<T>::size_type. Зная толькоsize_type(что в данном случае), как мы могли это выяснить? Мы не можем, потому что информация не содержится внутриstd::size_t.

Наиболее распространенный способ, с которым разработчики сталкиваются с этой проблемой, — это попытка вывести контейнер из итератора.

      template <typename T>
void foo(typename T::iterator it); // attempt to deduce T from an iterator

int main() {
    std::array<int> v;
    foo(v.begin()); // error, trying to deduce std::array<int> from int* (probably)
}                   // which is obviously imposssible

2.decltype(N)

Выражение спецификатора . _decltype

      template <int N>
void foo(decltype(N * N - N) n);

Выражение может быть сколь угодно сложным, иdecltypeспецификатор является типом. Если это параметр шаблона, не относящийся к типу, как мы можем узнать значение только по типу (например, догадаться123от )?

3.Foo<0 + N>,int(&)[0 + N]

Аргумент шаблона, не относящийся к типу, или привязанный массив, в котором подвыражение ссылается на параметр шаблона.

Можно было бы сделать вывод простоFoo<N>или из связанного массива, но когдаNпоявляется только как подвыражение, оно становится вообще невозможным. Теоретически это возможно для простого выражения, такого как0 + N, но это быстро выходит из-под контроля для более сложных выражений, таких какN * N.

4.void foo(T x = T{})

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

Если мы вызовем такую ​​функцию, какfoo(), то для вывода потребуется некоторая круговая логика. Тип будет выведен из аргумента по умолчанию.T{}, тип которого , который выводится из аргумента по умолчанию, ...


Примечание: этот случай относится к мотивирующему примеру в начале ответа.

5.void foo(T* function_pointer)с перегрузочными комплектами

Параметр функции, для которого связанный аргумент является набором перегрузки ( [over.over] ), и применимо одно или несколько из следующих условий:

  • более одной функции соответствует типу параметра функции (что приводит к неоднозначному выводу), или
  • ни одна функция не соответствует типу параметра функции, или
  • набор перегрузок, предоставленный в качестве аргумента, содержит один или несколько шаблонов функций.

Возможно, это не так очевидно, поэтому приведу пример:

      void take(int);
void take(float);

template <typename T>
void foo(T* function_pointer) { function_pointer(0); }
// note: T(*function_pointer)(int)  would work, and T would deduce to void

int main() { foo(&take); } // error

Обаtake(int)иtake(float)соответствовать параметру функции, поэтому неясно, является лиTдолжен прийти к выводуvoid(int)илиvoid(float).

6.std::initializer_listаргументы функции

Параметр функции, для которого связанным аргументом является список инициализаторов ( [dcl.init.list] ), но у параметра нет типа, для которого указан вывод из списка инициализаторов ( [temp.deduct.call] ).

Это хорошо демонстрирует пример в стандарте C++:

       template<class T> void g(T);
g({1,2,3});                 // error: no argument deduced for T

Из всех пуль это ограничение является наиболее искусственным.{1, 2, 3}можно было бы рассмотретьstd::initializer_list<int>, но было намеренно решено не делать этот вычет.

7.void foo(Ts..., int)

Пакет параметров функции, который не находится в конце списка объявлений-параметров .

Пакет параметров шаблонаTs...не может быть выведено. Основная проблема заключается в том, что если бы мы позвонилиfoo(0)это неоднозначно между предоставлением0в качестве аргумента для стаи или дляintпараметр. В шаблонах функций эта неоднозначность разрешается путем интерпретации пакета параметров как пустого пакета.

Дальнейшие примечания

Существует множество правил, которые необходимо соблюдать, чтобы вычет был возможен. Невыведение из невыведенного контекста — лишь один из них. [temp.deduct.type] p8 перечисляет формы, которые должен иметь тип, чтобы дедукция была возможна.

Еще одно косвенное правило, связанное с массивами, заключается в следующем:

      template <int N> foo(int(&)[N]); // N can be deduced from array size
template <int N> foo(int[N]);    // N cannot be deduced because arrays in parameters
                                 // are adjusted to pointers

Намеренное отключение вывода типа

Иногда разработчики намеренно отключают вычет, потому что хотят, чтобы пользователь шаблона функции явно предоставил аргумент. Это можно сделать с помощьюstd::type_identityв C++20 или в его пользовательской версии до C++20.

      template <typename T>
void foo(std::type_identity<T>::type); // non-deduced context in function parameter
Другие вопросы по тегам