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


102

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

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 2009年

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);
QuentinUK '16

18
C ++ 17将允许这样做!这项建议被接受:open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
underscore_d

1
@underscore_d太好了!时间到了!在我看来,这是它应该起作用的方式,而它却并非如此,这是很自然的。
amdn '16

Answers:


46

我认为这是无效的,因为构造函数并不总是该类的唯一入口(我说的是复制构造函数和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);

接受纸张


8
这实际上是我从未考虑过的重点。我看不到指针必须是特定于类型的事实(即,它必须是MyClass <string> * pm)。如果是这种情况,那么您最终要做的就是避免在实例化时指定类型。仅仅是一些额外工作的字符(并且仅当对象是按堆栈而不是按上面的方法在堆上创建时)。我一直怀疑类推断可能会打开蠕虫的句法罐,我想可能就是这样。
GRB

2
我不太清楚如何允许构造函数进行模板参数推断,而无需第二个代码行就需要允许不进行构造函数调用的非特殊声明。即,MyClass *pm由于以下原因,template <typename T> void foo();如果没有显式专门化就不能调用声明的函数,则此处将无效。
Kyle Strand

3
@KyleStrand是的,通过说“无法从其构造函数中推导出类模板参数,因为[不使用任何构造函数的示例] ”,此答案完全无关紧要。我真不敢相信它被接受了,达到+29,花了6年的时间才注意到这个明显的问题,并且整整7年没有一次投票就坐了下来。当他们阅读时,没有其他人会思考吗?
underscore_d

1
@underscore_d我喜欢目前的答案,它说:“该提案可能存在一些问题;我不确定我刚才说的是否有意义(!),请随时发表评论(!!);以及哦,顺便说一下,这几乎就是C ++ 17的工作方式。”
凯尔·斯特兰德

1
@KyleStrand啊,是的,这是另一个问题,我注意到了,但在其他所有乐趣中都没有提及。关于C ++ 17的编辑不是由OP进行的,也不应该批准IMO,而应将其发布为新答案:即使该帖子具有开始时毫无意义...我不知道编辑-全新的部分是公平的比赛,当然也没有那么激烈的编辑被拒绝,但是我想这是您获得哪些评论者的吸引力。
underscore_d

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设为静态成员函数。这就是人们所说的命名构造函数。因此,它不仅可以满足您的要求,而且几乎可以满足您的要求:编译器从(命名的)构造函数中推断出模板参数。

注意:当您编写类似文件时,任何合理的编译器都会优化掉临时对象

auto v = make_variable(instance);

6
要指出的是,在这种情况下使函数成为静态成员并不是特别有用,因为为此您必须为类指定模板参数以进行调用,因此推论它没有意义。
Predelnik

3
甚至在C ++ 11中,甚至可以做得更好,auto v = make_variable(instance)因此您实际上不必指定类型
-Claudiu

1
是的,大声static笑着宣布make函数作为成员的想法……请稍等片刻。抛开这些:免费的make函数确实解决方案,但这是很多冗余的样板,当您键入它时,您只知道不必这样做,因为编译器可以访问您要重复的所有信息。 ..幸好C ++ 17做到了这一点。
underscore_d

21

在开明的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已经正确的类型,所以推断是微不足道的。此外,在调用复制构造函数时不可能意外混合类型:

MyClass m(string("blah blah blah"));
auto pm = &(MyClass(m));

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

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

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

这是因为复制构造函数的声明模板化:

MyClass(const MyClass&);

在这里,copy-constructor参数的template-type 整个类的template-type 相匹配。即,何时MyClass<string>被实例化,MyClass<string>::MyClass(const MyClass<string>&);用它实例化,以及何时MyClass<int>被实例化,MyClass<int>::MyClass(const MyClass<int>&);被实例化。除非明确指定它或声明一个模板化的构造函数,否则编译器没有理由实例化MyClass<int>::MyClass(const MyClass<string>&);,这显然是不合适的。

CătălinPitiș的回答

皮蒂ș举了一个推论Variable<int>和的例子Variable<double>,然后说:

我在两种不同类型(变量和变量)的代码中具有相同的类型名(变量)。从我的主观角度来看,它在很大程度上影响了代码的可读性。

如前面的示例所述,即使新功能使它在语法上看起来像一个名称,Variable它本身也不是类型名称。

然后,Pitiș询问如果没有给出允许适当推断的构造函数,将会发生什么情况。答案是不允许推论,因为推论是由构造函数call触发的。没有构造函数调用,就没有推断

这类似于询问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>> ?

关键问题是,编译器是在这里选择类型推断的构造函数还是在复制构造函数?

尝试一下代码,我们可以看到选择了复制构造函数。扩展示例

Variable var(num);          // infering ctor
Variable var2(var);         // copy ctor
Variable var3(move(var));   // move ctor
// Variable var4(Variable(num));     // compiler error

我不确定提案和标准的新版本是如何指定的;它似乎由“演绎指南”决定,这是我尚不了解的一些新的标准语。

我也不确定为什么var4扣除是非法的。g ++的编译器错误似乎表明该语句已被解析为函数声明。


多么棒的详细答案! var4只是“最烦人的解析”(与模板arg推论无关)的一种情况。我们过去常常为此使用多余的括号,但是如今,我认为使用括号明确表示构造是通常的建议。
Sumudu Fernando

@SumuduFernando谢谢!您是说Variable var4(Variable(num));将其视为函数声明吗?如果是这样,为什么要Variable(num)使用有效的参数说明?
凯尔·斯特兰德'18

@SumuduFernando没关系,我不知道这是有效的:coliru.stacked-crooked.com/a/98c36b8082660941
Kyle Strand

11

仍然缺少:它使以下代码变得很模糊:

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 2009年

1
或者,或者让编译器再次引发未定义的模板参数错误,就像我对Pitis的答案所建议的那样。但是,如果采用该路线,则可以进行无问题(错误)推理的次数越来越少。
GRB 2009年

这实际上是一个有趣的观点,而且(正如我在回答中所指出的那样),我不确定所接受的C ++ 17提案如何解决这一问题。
凯尔·斯特兰德

9

假设编译器支持您的要求。那么此代码是有效的:

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

// Some code here

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

现在,我在代码中为两种不同类型(变量和变量)使用了相同的类型名(变量)。从我的主观角度来看,它在很大程度上影响了代码的可读性。在同一个名称空间中为两个不同的类型使用相同的类型名称似乎对我产生了误导。

以后更新: 需要考虑的另一件事:部分(或全部)模板专业化。

如果我专门研究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

这是一个很好的观点,但可以通过强制转换来轻松地超越它:Variable v1((double)10)
jpinto3912

我同意代码的可读性是一个主观问题,但是,我同意您对模板专业化的要求100%。解决方案可能是给出未定义的模板参数错误(一旦编译器查看了<int>特化,并且没有看到有效的构造函数,让它说它不知道要使用哪个模板,并且必须明确指定),但是我同意这不是一个很好的解决方案。我将其添加为另一个需要处理的主要语法漏洞(但如果接受后果则可以解决)。
GRB

4
@ jpinto3912-您错过了重点。编译器必须实例化所有可能的Variable <T>,以检查是否有任何ctor Variable <T> :: Variable提供了模糊的ctor。摆脱歧义不是问题-如果需要的话,可以自己简单地实例化Variable <double>。首先发现模棱两可,这使得它变得不可能。
MSalters

6

C ++ 03和C ++ 11标准不允许从传递给构造器的参数中推导模板参数。

但是对于“构造函数的模板参数推导”有一个建议,这样您就可以很快获得所需的信息。编辑:确实,此功能已为C ++ 17确认。

请参阅:http : //www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3602.htmlhttp://www.open-std.org/jtc1/sc22/wg21/docs/论文/2015/p0091r0.html


该功能已添加到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标准固定周转率),然后编译器提供商才能添加此功能


对于即将推出的标准,引入自动关键字是不正确的。看一下James Hopkins在该主题中的帖子。stackoverflow.com/questions/984394/…。他展示了在C ++ 0x中它将如何实现。
ovanes

1
只是为了纠正自己,auto关键字也出现在当前标准中,但出于不同的目的。
ovanes

看起来将是8年(从回答这个问题开始)……所以10年并不是一个坏的猜测,即使在此期间有两个标准!
Kyle Strand

-1

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

首先,向量的一种非常普遍的用法是使用不带参数的构造函数:

vector <int> v;

在这种情况下,显然无法进行推断。

第二个常见用途是创建一个预先设置大小的向量:

vector <string> v(100);

在这里,如果使用推断:

vector v(100);

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

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

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

应该使用哪个参数进行推断?我们需要某种方式告诉编译器它应该是第二种。

对于像vector这样简单的类,由于存在所有这些问题,因此很容易理解为什么不使用推理。


3
我认为您误解了这个主意。仅当模板类型是构造函数的一部分时,才会发生构造函数的类型推断。假设vector具有模板定义template <typename T>。您的示例没有问题,因为vector的构造函数将被定义为vector(int size),而不是vector(T size)。仅在向量(T大小)的情况下,才会发生任何推断;在第一个示例中,编译器将给出错误消息,指出T未定义。与功能模板推断的工作原理基本相同。
GRB

因此,仅对具有单个参数且该参数是模板参数类型的构造函数进行此操作?似乎实例的数量几乎消失了。

它不一定是单个参数。例如,可能有一个向量构造函数vector(int size,T firstElement)。如果模板具有多个参数(template <typename T,typename U>),则可以具有Holder :: Holder(T firstObject,U secondObject)。如果模板具有多个参数,但构造函数仅使用其中一个,例如Holder(U secondObject),则始终必须明确声明T。规则将旨在尽可能类似于功能模板推断。
GRB 2009年

-2

使ctor成为模板,变量可以只有一种形式,但是可以有多种ctor :

class Variable {
      obj data; // let the compiler guess
      public:
      template<typename obj>
      Variable(obj d)
       {
           data = d;
       }
};

int main()
{
    int num = 2;
    Variable var(num);  // Variable::data int?

    float num2 = 2.0f;
    Variable var2(num2);  // Variable::data float?
    return 0;         
}

看到?我们不能有多个Variable :: data成员。


在任何情况下,这都是没有意义的。由于obj不再是模板,因此在obj数据方面未定义obj。无论哪种方式,这样的代码都是无效的。
GRB

我想要您描述的编译器行为,所以我想出了一种绕过该限制的方法(在我的情况下),您可能会发现有趣的stackoverflow.com/questions/228620/garbage-collection-in-c-why / ...
Nick Dandoulakis 09年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.