为什么不能从构造函数推断模板参数?

117

今天我要问的问题很简单:为什么编译器不能像从函数参数中一样从类构造函数中推断模板参数呢?例如,为什么下面的代码不能有效:

template <typename obj>
class Variable {
    obj data;
public:
    Variable(obj d) { data = d; }
};

int main() {
    int num = 2;
    Variable var(num); // would be equivalent to Variable<int> var(num),
    return 0;          // but actually a compile error
}

就像我说的,我理解这不是有效的,所以我的问题是,为什么不是有效的?允许这样做会产生任何主要的语法漏洞吗?是否存在这样的情况,其中一个不希望此功能(推断类型会导致问题)?我只是想理解允许函数模板推断而不适用于适当构造的类背后的逻辑。


我想邀请某人(我可以做到,只是现在不行),编译Drahakar和Pitis的答案(至少)作为为什么它不能工作的好反例。 - jpinto3912
2
还要注意,这可以通过以下方式轻松解决:template<class T> Variable<T> make_Variable(T&& p) {return Variable<T>(std::forward<T>(p));} - Mooing Duck
3
你可以使用这段代码来实现你想要的效果:var = Variable<decltype(n)>(n)。其中,变量 var 是通过类型推断得到的,并且构造了一个名为 Variable 的对象,使用了模板参数 decltype(n),并将 n 作为构造函数的参数传递进去。 - QuentinUK
21
C++17可以实现这个功能!这个提案已被接受:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html - underscore_d
1
@underscore_d 太好了!早就该这样了!对我来说,这是自然而然的事情,应该是这样的方式,而它没有这样做是令人烦恼的来源。 - amdn
12个回答

51

我认为这不是有效的,因为构造函数并不总是类的唯一入口点(我指的是复制构造函数和operator=)。所以假设您像这样使用类:

MyClass m(string s);
MyClass *pm;
*pm = m;

我不确定解析器是否能够明确知道MyClass pm的模板类型。

不确定我的话是否有意义,但随意添加一些评论,这是一个有趣的问题。

C++ 17

已经接受了C++17将从构造函数参数中进行类型推导。

示例:

std::pair p(2, 4.5);
std::tuple t(4, 3, 2.5);

被接受的论文


9
这实际上是一个我从未考虑过的很好的观点。我看不出有任何方法可以避免指针必须是特定类型(即它必须是MyClass<string>* pm)。如果是这样,那么你最终只会省去在实例化时指定类型的工作;这只是多了一些少量的字符工作(而且仅当对象在堆栈上创建时才是如此,如上所述)。我一直怀疑类推断可能会引发一些语法上的问题,我想这可能就是其中之一。 - GRB
3
我不太明白允许从构造函数进行模板参数推断如何需要允许没有构造函数调用的非特化声明,就像你第二行所说的那样。也就是说,在这里声明MyClass *pm将无效,原因与声明template <typename T> void foo();的函数不能在没有显式特化的情况下被调用相同。 - Kyle Strand
3
@KyleStrand,是的,通过说“由于_[未使用任何构造函数的示例]_,类模板参数无法从它们的构造函数中推导出来”,这个答案完全跟问题无关。我真的不敢相信它被接受了,得到了+29的赞成,花了6年时间才有人注意到明显的问题,并且在7年内没有任何负面评价。难道没有其他人在阅读时思考吗,或者......? - underscore_d
1
@KyleStrand 哦,是的,这又是另一个问题,我在所有其他有趣的事情中注意到了它,但忘记提及。关于 C++17 的编辑不是由 OP 进行的... 在我看来,不应该被批准,而应该作为新答案发布:即使帖子一开始就没有意义,它也可以被拒绝,因为它“改变了帖子的含义”... 我不知道编辑全新部分是否公平竞争,肯定有更少的激烈编辑被拒绝,但我想这取决于你得到哪些审阅者的运气。 - underscore_d
1
@underscore_d,我已经添加了一个更加深入的答案。 - Kyle Strand
显示剩余13条评论

27

出于其他人已经提到的原因,您无法做您要求的事情,但您可以做以下操作:

template<typename T>
class Variable {
    public: Variable(T d) {}
};
template<typename T>
Variable<T> make_variable(T instance) {
  return Variable<T>(instance);
}

从所有意义上讲,这就是你所请求的相同东西。

如果你喜欢封装,你可以将make_variable设为静态成员函数。这就是人们所谓的命名构造函数。因此它不仅可以完成你想要的功能,而且几乎被称为你想要的:编译器正在从(命名的)构造函数中推断模板参数。

NB: 任何合理的编译器都会在像下面这样写的时候优化掉临时对象

auto v = make_variable(instance);

7
想要指出,在这种情况下将函数设为静态成员并没有特别的用处,因为你仍然需要为类指定模板参数才能调用它,所以推导模板参数就没有意义了。 - Predelnik
3
在C++11中,你甚至可以使用auto v = make_variable(instance),这样你就不需要实际指定类型,这样做更好。 - Claudiu
2
是的,声明make函数为“static”成员的想法真是太可笑了...只需要再思考一秒钟就可以明白。除此之外:免费的make函数确实是解决方案,但这是很多冗余样板代码,当你在输入它时,你就知道你不应该这样做,因为编译器可以访问你正在重复的所有信息...幸运的是,C++17将其正式化。 - underscore_d

27
在2016年这个开明的时代,自从提出这个问题以来,我们已经有了两个新的标准,并且还有一个新的标准即将发布。关键要知道的是,支持C++17标准的编译器将会原样编译您的代码。
C++17中的类模板模板参数推导
这里(感谢Olzhas Zhumabek编辑的答案)详细介绍了相关标准的变化。
解决其他答案中的问题
当前排名第一的答案指出,“复制构造函数和operator=”无法知道正确的模板特化。
这是荒谬的,因为标准的复制构造函数和operator=只存在于已知的模板类型中。
template <typename T>
class MyClass {
    MyClass(const MyClass&) =default;
    ... etc...
};

// usage example modified from the answer
MyClass m(string("blah blah blah"));
MyClass *pm;   // WHAT IS THIS?
*pm = m;

在这里,正如我在评论中指出的那样,没有理由让MyClass *pm成为一个合法的声明,无论是否使用新形式的推断:MyClass不是一种类型(它是一个模板),所以声明一个MyClass类型的指针是没有意义的。以下是修复示例的一种可能方式:
MyClass m(string("blah blah blah"));
decltype(m) *pm;               // uses type inference!
*pm = m;

在这里,pm已经是正确的类型,因此推理是微不足道的。此外,在调用复制构造函数时,不可能意外地混合类型:mix
MyClass m(string("blah blah blah"));
auto pm = &(MyClass(m));

这里,pm将是指向m的副本的指针。这里,MyClass正在从类型为MyClass<string>(而不是不存在的类型MyClass)的m进行复制构造。因此,在推断pm的类型的点上,有足够的信息可以知道m的模板类型,因此pm的模板类型是string

此外,以下内容始终会引发编译错误

MyClass s(string("blah blah blah"));
MyClass i(3);
i = s;

这是因为复制构造函数的声明不是模板化的。
MyClass(const MyClass&);

这里,复制构造函数的参数模板类型“匹配”类的整体模板类型;即,当实例化MyClass<string>时,将与之一起实例化MyClass<string>::MyClass(const MyClass<string>&),而当实例化MyClass<int>时,将与之一起实例化MyClass<int>::MyClass(const MyClass<int>&)。除非明确指定或声明了一个模板化构造函数,否则编译器没有理由实例化MyClass<int>::MyClass(const MyClass<string>&),那显然是不合适的。

Cătălin Pitiș的答案

Pitiș给出了推断Variable<int>Variable<double>的示例,然后陈述:

我在代码中有两种不同类型(Variable和Variable)的相同类型名(Variable)。从我的主观角度来看,这对代码的可读性影响很大。

正如前面的例子所述,Variable本身并不是一个类型名称,尽管新特性在语法上让它看起来像是一个。

Pitiș随后问如果没有提供允许适当推断的构造函数会发生什么。答案是没有推断被允许,因为推断是由构造函数调用触发的。没有构造函数调用,则没有推断

这类似于询问在这里推导出了哪个版本的foo

template <typename T> foo();
foo();

答案是,这段代码是非法的,原因如上所述。
MSalter的回答
据我所知,这是唯一一个提出对所提议功能的合理关注的答案。
示例为:
Variable var(num);  // If equivalent to Variable<int> var(num),
Variable var2(var); // Variable<int> or Variable<Variable<int>> ?

关键问题是,编译器在这里选择的是类型推断构造函数还是拷贝构造函数?
通过尝试代码,我们可以看到选择了拷贝构造函数。为了进一步说明这个例子
int num = 3;
Variable var(num);            // infering ctor
Variable var2(var);           // copy ctor
Variable var3(move(var));     // move ctor
Variable var4{Variable(num)}; // infering ctor
// Variable var4(Variable(num));     // illegal

我不确定提案和新版本的标准如何具体规定这一点;它似乎是由“推导指南”决定的,这是一种我尚未理解的新标准术语。
由于“最令人烦恼的解析”(该语句被解析为函数声明),var4的推导是非法的。我不完全确定原因,因为对我来说,这看起来不像一个有效的函数声明语法。

多么出色、详细的答案啊!var4 只是“最令人烦恼的解析”之一(与模板参数推导无关)。我们过去通常会使用额外的括号来解决这个问题,但现在我认为使用大括号来明确表示构造是通常的建议。 - Sumudu Fernando
@SumuduFernando谢谢!您的意思是Variable var4(Variable(num));被视为函数声明吗?如果是,那么为什么Variable(num)是有效的参数说明呢? - Kyle Strand
@SumuduFernando 没关系,我不知道这是有效的:http://coliru.stacked-crooked.com/a/98c36b8082660941 - Kyle Strand

12

仍有缺失:它使得以下代码相当模糊:

int main()
{
    int num = 2;
    Variable var(num);  // If equivalent to Variable<int> var(num),
    Variable var2(var); //Variable<int> or Variable<Variable<int>> ?
}

另一个好处是,假设存在定义为Variable(Variable <obj> d)的复制构造函数,则必须建立某种优先级。 - GRB
1
或者,作为另一种选择,让编译器再次抛出未定义的模板参数错误,就像我在对Pitis的回答提出的建议一样。然而,如果你选择这条路线,推断可以顺利进行(没有错误)的次数将越来越少。 - GRB
这实际上是一个有趣的观点,而且(正如我在我的答案中所指出的)我还不确定被接受的C++17提案如何解决这个问题。 - Kyle Strand

9

假设编译器支持你所要求的内容,那么这段代码就是有效的:

Variable v1( 10); // Variable<int>

// Some code here

Variable v2( 20.4); // Variable<double>

现在,我的代码中有两种不同类型(Variable和Variable)的相同类型名称(Variable)。从我主观的角度来看,这对代码的可读性产生了很大的影响。在同一命名空间中为两种不同类型使用相同的类型名称对我来说是具有误导性的。
后续更新: 还有一件事要考虑:部分(或全部)模板特化。
如果我特化Variable并且不像你期望的那样提供构造函数呢?
所以我会有:
template<>
class Variable<int>
{
// Provide default constructor only.
};

然后我有这段代码:

Variable v( 10);

编译器应该做什么?使用通用的Variable类定义来推断它是Variable,然后发现Variable没有提供一个参数构造函数吗?

1
更糟糕的是:如果你只有Variable<int>::Variable(float)呢?现在你有两种方法来推断Variable(1f),但却没有办法推断Variable(1)。 - MSalters
这是一个很好的观点,但可以通过强制转换轻松地超越:变量v1((double)10)。 - jpinto3912
我同意代码可读性是一个主观问题,但是我完全同意你在模板特化方面的观点。解决方案可能是给出未定义的模板参数错误(一旦编译器查看<int>特化并发现没有有效的构造函数,让它说它不知道你想使用哪个模板,并且必须明确指定),但我同意这不是一个好的解决方案。我会把这作为另一个需要处理的主要语法漏洞(但如果接受后果,可以解决)。 - GRB
4
@jpinto3912 - 你没有抓住重点。编译器必须实例化所有可能的Variable<T>,以检查是否有任何模板构造函数Variable<T>::Variable提供了模棱两可的构造函数。消除模棱两可并不是问题 - 如果需要的话,只需手动实例化Variable<double>即可。但首先找到这种模棱两可才是困难的。 - MSalters

6

该功能已添加到C++17中,但不确定“不久”是否适用于6至8年的时间范围。 ;) - ChetS

2
很多类不依赖于构造函数参数。只有少数类仅有一个构造函数,并基于该构造函数的类型进行参数化。
如果您确实需要模板推断,请使用辅助函数:
template<typename obj>
class Variable 
{
      obj data;
public: 
      Variable(obj d)
      : data(d)
      { }
};

template<typename obj>
inline Variable<obj> makeVariable(const obj& d)
{
    return Variable<obj>(d);
}

1
当然,这个功能只对某些类有用,但同样适用于函数推断。并非所有模板函数都从参数列表中获取其参数,但是我们允许那些使用参数列表的函数进行推断。 - GRB

1
类型推导在当前 C++ 中仅限于模板函数,但早已意识到在其他情况下进行类型推导将非常有用。因此 C++0x 引入了 `auto`。
虽然在 C++0x 中无法实现您所建议的“完全相同”,但以下内容显示您可以接近实现:
template <class X>
Variable<typename std::remove_reference<X>::type> MakeVariable(X&& x)
{
    // remove reference required for the case that x is an lvalue
    return Variable<typename std::remove_reference<X>::type>(std::forward(x));
}

void test()
{
    auto v = MakeVariable(2); // v is of type Variable<int>
}

0

你说得对,编译器可以轻松猜测,但据我所知,这不是标准或C++0x的一部分,因此您将不得不等待至少10年(ISO标准固定周转率)才能让编译器提供商添加此功能。


这并不正确,随着即将推出的标准,auto关键字将被引入。请查看James Hopkins在此线程中的帖子。https://dev59.com/53NA5IYBdhLWcg3wYMt-#986197。他展示了在C++0x中如何实现。 - ovanes
1
只是纠正一下自己,auto关键字也存在于当前的标准中,但用途不同。 - ovanes
看起来需要8年时间(从回答时算起)……所以即使期间出现了两个标准,10年也不是一个坏的猜测! - Kyle Strand

-1

让我们以一个每个人都应该熟悉的类 - std::vector为例来看看问题。

首先,vector的一个非常常见的用法是使用不带参数的构造函数:

vector <int> v;

在这种情况下,显然无法执行任何推断。
第二个常见用途是创建预定大小的向量:
vector <string> v(100);

如果使用推理:

vector v(100);

我们得到的是一个整数向量,而不是字符串,并且可能没有确定大小!

最后,考虑带有“推断”的多参数构造函数:

vector v( 100, foobar() );      // foobar is some class

哪个参数应该用于推理?我们需要某种告诉编译器应该是第二个参数的方法。
对于一个简单的向量类来说,有这么多问题,很容易看出为什么不使用推理。

3
我觉得你误解了这个想法。如果模板类型是构造函数的一部分,那么才会发生构造函数的类型推断。假设vector有模板定义template<typename T>。你的例子并不是一个问题,因为vector的构造函数将被定义为vector(int size),而不是vector(T size)。只有在vector(T size)的情况下才会发生任何推断;在第一个例子中,编译器会报错说T未定义,与函数模板推断方式基本相同。 - GRB
那么这只会发生在只有一个参数且该参数是模板参数类型的构造函数中?这似乎是非常少见的情况。 - anon
它不一定是单个参数。例如,可以有一个向量构造函数vector(int size, T firstElement)。如果模板有多个参数(template<typename T, typename U>),则可以有Holder::Holder(T firstObject, U secondObject)。如果模板有多个参数但构造函数只接受其中之一,例如Holder(U secondObject),那么T始终必须明确声明。规则旨在尽可能类似于函数模板推断。 - GRB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接