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

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

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 } 

正如我所说,我明白这是无效的,所以我的问题是为什么不是? 会允许这个创build任何主要的语法漏洞? 是否有一个不希望这个function的实例(推断types会导致问题)? 我只是想了解允许模板推理function背后的逻辑,但不适合适当构build的类。

我认为这是无效的,因为构造函数并不总是类的唯一入口点(我正在谈论复制构造函数和运算符=)。 所以假设你正在使用你的class级:

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

我不确定parsing器是否明显知道MyClass pm的模板types是什么;

不知道我说的是否有意义,但随意添加一些评论,这是一个有趣的问题。

C ++ 17

可以接受的是,C ++ 17将从构造函数参数中进行types推导。

例子:

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

被接受的论文 。

你不能因为其他人的理由而要求你做什么,但你可以这样做:

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

对于所有意图和目的而言,这都是你所要求的。 如果你喜欢封装,你可以使make_variable成为一个静态成员函数。 这就是人们所称的命名构造函数。 所以它不仅可以做你想做的事情,而且几乎可以调用你想要的:编译器从(named)构造函数中推断出模板参数。

注意:当你编写类似的东西的时候,任何合理的编译器都会优化临时对象

 Variable<T> v = make_variable(instance); 

在2016年的启蒙时代,由于提出了这个问题并提出了一个新的标准,有两个新的标准,要知道关键的是支持C ++ 17标准的编译器将按原样编译您的代码

C ++中类模板的模板参数推导17

这里 (由Olzhas Zhumabek编辑接受的答案)是详细描述标准相关变化的文件。

解决其他问题的疑虑

目前评分最高的答案

这个答案指出“复制构造函数和operator= ”不会知道正确的模板专业化。

这是无稽之谈,因为标准的copy-constructor和operator= 只存在已知的模板types中:

 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成为具有或不具有新推理forms的合法声明: MyClass 不是一个types (它是一个模板),所以它没有任何意义声明一个MyClasstypes的指针。 以下是修复该示例的一种可能方法:

 MyClassstring m("blah blah blah")); decltype(m) *pm; // uses type inference! *pm = m; 

在这里, pm 已经是正确的types,所以推断是微不足道的。 而且,在调用复制构造函数时不可能意外地混合types:

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

在这里, pm将是一个指向m的副本的指针。 在这里, MyClass是从mtypes为MyClass<string> (而不是不存在的typesMyClass )复制构build的。 因此,在pmtypes被推断的地方,有足够的信息来知道m的模板types,因此pm的模板types是string

而且,下面总会 引起一个编译错误 :

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

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

 MyClass(const MyClass&); 

这里,copy-constructor参数的模板types整体类的模板types相匹配 ; 即,当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ş的答复

Pitiş给出了一个推导Variable<int>Variable<double>的例子,然后说明:

在两种不同types(variables和variables)的代码中,我有相同的types名称(variables)。 从我的主观观点来看,它几乎影响了代码的可读性。

正如前面的例子所指出的那样, Variable本身并不是一个types名称,尽pipe这个新特性使得它看起来像一个语法上的。

Pitiş然后问如果没有构造函数将允许适当的推论会发生什么。 答案是不允许推理,因为推理是由构造函数调用触发的。 没有构造函数,就没有推理

这与在此推断foo版本是相似的:

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

答案是这个代码是非法的,因为这个原因。

MSalter的回答

就我所知,这是唯一的答案,就提出的function提出合理的关注。

例子是:

 Variable var(num); // If equivalent to Variable<int> var(num), Variable var2(var); //Variable<int> or Variable<Variable<int>> ? 

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

试一试代码,我们可以看到复制构造函数被选中。 展开这个例子 :

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

我不确定提案和标准的新版本是如何规定的; 这似乎是由“扣除指南”决定的,这是我还不了解的一个新的标准。

我也不确定为什么var4演绎是非法的。 来自g ++的编译器错误似乎表明该语句正在被parsing为一个函数声明。

仍然缺less:它使下面的代码很模糊:

 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 v1( 10); // Variable<int> // Some code here Variable v2( 20.4); // Variable<double> 

现在,在两种不同types(variables和variables)的代码中,我有相同的types名称(variables)。 从我的主观观点来看,它几乎影响了代码的可读性。 同一个名字空间中两个不同types的同名types对我来说看起来有些误导。

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

如果我专门研究variables并且不提供像你期望的构造函数呢?

所以我会有:

 template<> class Variable<int> { // Provide default constructor only. }; 

然后我有这个代码:

 Variable v( 10); 

编译器应该做什么? 使用genericsvariables类定义来推断它是variables,然后发现该variables不提供一个参数构造函数?

C ++ 03和C ++ 11标准不允许从传递给构造函数的参数中减去模板参数。

但有一个build议“模板参数扣除build设者”,所以你可能会得到你很快要求。 编辑:的确,这个function已经被C ++ 17证实了。

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

许多类不依赖于构造函数参数。 只有几个类只有一个构造函数,并且基于这个构造函数的types进行参数化。

如果你真的需要模板推理,使用一个辅助函数:

 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); } 

types的演绎仅限于当前C ++中的模板函数,但人们早已认识到,在其他上下文中的types演绎将非常有用。 因此,C ++ 0x的auto

正如你所build议的,在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> } 

你试图实现的是所谓的types擦除。 看看boost :: any,它的实现是如何工作的。

提升任何

现在到这个问题,为什么它不工作。 作为boost :: any可以实现。 它的工作,但你的问题将是调度哪种types是真正的内部。 与模板方法相反,您将被要求在应用程序运行时进行调度,因为包含的types将被删除。 有两种可能性来处理:访问者模式和自定义强制转换实现(如果您试图转换为错误types,则会引发exception)。 在这两种情况下,您都将编译时types安全性移至运行时并绕过编译器的types检查。

另一种方法是引入Varianttypes: Boost Variant

Variant与boost :: any的工作方式不同,只允许有限数量的types存储。 这会引入更高的types安全性,因为您确实限制了预期types的​​集合。 Andrey Alexandrescu在ddj上写了一篇很好的文章,讲述如何实现这样一个变体: 第1 部分 , 第2 部分 , 第3部分

它的实现比boost :: any更复杂,但提供更高的types安全性,并且不允许用户将任何可能的types放入变体中,除了那些被明确声明的types。

正如我所说,它可以在C ++中实现,但需要深入的语言知识和良好的界面devise,使该类的用户不把苹果当作桃子。

请记住亨利·斯宾塞的话:如果你对编译器说谎,它会报复。

问候,

Ovanes


实施理念(!未经testing!)

 class any_type { class any_container { public: virtual ~any_container(){} virtual void* pointer()=0; virtual type_info const& get_type()const=0; }; template<class T> class any_container_impl { T t_; public: any_container_impl(T const& t) : t_(t) {} virtual ~any_container_impl(){} virtual void* pointer() { return &t_; } virtual type_info const& get_type()const { return typeid(T); } }; std::auto_ptr<any_container> content_; public: template<class T> any_type(T const& t) : content_(new any_container_impl<T>(t)) {} template<class T> T* cast_to_ptr() { if(typeid(T)!=content_->get_type()) return NULL; return reinterpret_cast<T*>(content_->pointer()); } template<class T> T& cast_to_ref() { T* ptr = cast_to_ptr<T>(); if(!ptr) throw std::logic_error("wrong type"); return *ptr; } }; 

编译器可以很容易地猜到,但它不在标准或C ++ 0x中,据我所知,所以在编译器提供程序添加此function之前,您必须等待至less10年(ISO标准固定转身速率)

让我们看看引用一个类的问题,每个人都应该熟悉 – std :: vector。

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

 vector <int> v; 

在这种情况下,显然不能进行推理。

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

 vector <string> v(100); 

在这里,如果使用推论:

 vector v(100); 

我们得到一个整数的vector,而不是string,可能它不是大小!

最后,考虑具有多个参数的构造函数 – “推理”:

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

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

对于像vector这样简单的类来说,所有这些问题都很容易理解为什么不使用推理。

使用ctor作为模板,variables只能有一种forms,但只有一种forms

 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成员。

有关详情,请参阅C ++模板参数推导 。