Что такое не выводимый контекст?
Недавно я столкнулся с этим вопросом, и ответы на него можно суммировать как "Это не выводимый контекст".
В частности, первый говорит, что это такая вещь, а затем перенаправляет на стандарт для "деталей", а второй цитирует стандарт, который, по меньшей мере, загадочен.
Может ли кто-нибудь объяснить простым смертным, таким как я, что такое недооцененный контекст, когда это происходит и почему это происходит?
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)
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