什么是非演绎上下文?

What is a non-deduced context?

提问人:Shoe 提问时间:8/11/2014 最后编辑:Jan SchultkeShoe 更新时间:10/3/2023 访问量:9098

问:

我偶然发现了为什么模板参数推导在这里不起作用? 最近,答案可以归结为“这是一个非推导的上下文”。

具体来说,第一个说它是这样的事情,然后重定向到“细节”的标准,而第二个引用了标准,至少可以说是神秘的。

有人可以向像我这样的凡人解释一下,什么是非推导的背景,它何时发生,以及为什么发生?

C++ 模板 template-argument-deduction C++20

评论

2赞 Shafik Yaghmour 8/11/2014
相关:C++,模板参数无法推导

答:

123赞 Kerrek SB 8/11/2014 #1

演绎是指从给定参数中确定模板参数类型的过程。它适用于函数模板和其他一些情况(例如部分专用化)。例如,请考虑:auto

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

现在,如果你说,你声明的地方,那么被推导出为 ,你就得到了专业化。f(x)std::vector<int> x;Tintf<int>

为了使演绎起作用,要演绎的模板参数类型必须出现在可演绎的上下文中。在此示例中,函数参数 是这样一个可推导的上下文。也就是说,函数调用表达式中的参数允许我们确定模板参数应该是什么才能使调用表达式有效。fT

但是,也存在无法推导的上下文,无法进行推导。规范示例是“出现在 :::

template <typename> struct Foo;

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

在此函数模板中,函数参数列表中的 in 位于非推导上下文中。因此,你不能说和推断.这样做的原因是任意类型和成员之间没有“反向对应”。例如,您可以有专业化:Tg(x)TFoo<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{})Tg(int{})


有时,显式地禁止参数演绎是有用的。例如,就是这种情况。另一个例子是,当您有从 到 的转换时,比如说,或其他转换(想想 和 )。现在假设你有一个自由函数:std::forwardFoo<U>Foo<T>std::stringchar const *

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

如果调用,则推导可能模棱两可,因此失败。但是,只推导出一个论点而推导出另一个论点是合理的,因此允许隐式转换。现在需要一个显式非推导的上下文,例如:binary_function(t, u)

template <typename T>
struct type_identity {
    using type = T;
};

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

(您可能遇到过这样的推导问题。std::min(1U, 2L)

注意:std::type_identity 从 C++20 开始在标准库中可用。

评论

0赞 Shoe 8/11/2014
因此,您引用的是标准描述的第二种情况(“作为模板 ID 的类型,其中一个或多个模板参数是引用模板参数的表达式”),对吗?如果是这样,您能否举出第一个示例(“使用限定 ID 指定的类型的嵌套名称说明符”)?
1赞 Shoe 8/11/2014
其实,现在我仔细读了,我觉得恰恰相反。
0赞 Kerrek SB 8/11/2014
@Jefffrey:也许是这样的?template <std::size_t> struct Bar; template <typename T> void(Bar<sizeof(T)>);
9赞 Kerrek SB 8/11/2014
关键是:类型和模板类之间存在一对一的对应关系,因此您可以从后者推断出前者。但是类型和任意成员之间没有对应关系。TFoo<T>TFoo<T>::X
1赞 Kerrek SB 10/27/2018
@camino:不,我不会这么说——这不是技术或机械问题。原则上有一个更根本的问题,即从名称到类型的映射是不可逆的,因此采用类型并要求定义它的“the”名称是没有意义的,因为这个概念并不存在。演绎只适用于更狭隘的问题,即“哪种类型可以被替换到这个特定的给定模式中以形成有效的调用”,这个问题受到的约束要大得多,因此很有意义。
0赞 Jan Schultke 9/10/2023 #2

什么是非演绎上下文?

非推导上下文是无法推导模板参数的上下文。 从某些构造中推导出模板参数并不总是可行的。 例如:

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

显然不可能从默认参数中推导出非类型模板参数。编译器不仅要取 的平方根,而且也不清楚它首先从哪里获得 的值。NN * NNN * N

非推导上下文列表

在下面的列表中,假设

  • T是一些模板类型参数
  • N是一些非类型的模板参数
template <typename T, int N>

每个标题都引用并解释了 [temp.deduct.type] p5 中的一个项目符号。

1.Foo<T>::type

使用 qualified-id 指定的类型的 nested-name-specifier

没有办法推断,因为只是一个别名,例如 。只知道(在这种情况下),我们怎么可能弄清楚?我们不能,因为信息不包含在 .Ttypestd::vector<T>::size_typesize_typestd::size_tTstd::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-specifier表达式

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

的表达式可以是任意复杂的,说明符是一个类型。如果是一个非类型模板参数,我们怎么可能只从类型(例如猜测)中知道值?decltypedecltypeN123int

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

非类型模板参数或子表达式引用模板参数的绑定数组。

从 just 或从绑定的数组中推导出来是可能的,但是当仅作为子表达式出现时,它通常变得不可能。从理论上讲,对于像 这样的简单表达式是可能的,但对于更复杂的表达式(如 )来说,这很快就会失控。NFoo<N>N0 + NN * N

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

在函数参数的参数类型中使用的模板参数,该参数具有默认参数,该参数在要执行参数推导的调用中使用。

如果我们将这样的函数称为 ,那么演绎将需要一些循环逻辑。该类型将从默认参数推断出来,其类型是 ,这是从默认参数 ...foo()TT{}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)Tvoid(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)

不在 parameter-declaration-list 末尾出现的函数参数包。

无法推断模板参数包。 基本问题是,如果我们调用它,那么在作为参数提供给包或参数之间是模棱两可的。 在函数模板中,通过将参数包解释为空包来解决这种歧义。Ts...foo(0)0int

更多说明

需要遵循许多规则才能进行扣除。 不从非推导的上下文中推导只是其中之一。[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

有意禁用类型推导

有时,开发人员会故意禁用推导,因为他们希望函数模板的用户显式提供参数。 这可以通过 C++20 中的 std::type_identity 或 C++20 之前的用户定义版本来完成。

template <typename T>
void foo(std::type_identity<T>::type); // non-deduced context in function parameter