隐式构造函数可用于从 Base 派生的所有类型(当前类型除外)?

Posted

技术标签:

【中文标题】隐式构造函数可用于从 Base 派生的所有类型(当前类型除外)?【英文标题】:Implicit constructor available for all types derived from Base excepted the current type? 【发布时间】:2012-08-24 11:39:47 【问题描述】:

以下代码总结了我的问题:

template<class Parameter>
class Base ;

template<class Parameter1, class Parameter2, class Parameter>
class Derived1 : public Base<Parameter>
 ;

template<class Parameter1, class Parameter2, class Parameter>
class Derived2 : public Base<Parameter>

public :
    // Copy constructor
    Derived2(const Derived2& x);

    // An EXPLICIT constructor that does a special conversion for a Derived2
    // with other template parameters
    template<class OtherParameter1, class OtherParameter2, class OtherParameter>
    explicit Derived2(
        const Derived2<OtherParameter1, OtherParameter2, OtherParameter>& x
    );

    // Now the problem : I want an IMPLICIT constructor that will work for every
    // type derived from Base EXCEPT
    // Derived2<OtherParameter1, OtherParameter2, OtherParameter> 
    template<class Type, class = typename std::enable_if</* SOMETHING */>::type>
    Derived2(const Type& x);
;

考虑到我已经有示例代码中的显式构造函数,如何将隐式构造函数限制为从父类派生的所有类(当前类除外),无论其模板参数是什么?

编辑: 对于 Base 的隐式构造函数,我显然可以这样写:

template<class OtherParameter> Derived2(const Base<OtherParameter>& x);

但是在那种情况下,我是否可以保证编译器不会将此构造函数用作Derived2&lt;OtherParameter1, OtherParameter2, OtherParameter&gt; 的隐式构造函数?

编辑2: 在这里我有一个测试:(LWS在这里:http://liveworkspace.org/code/cd423fb44fb4c97bc3b843732d837abc)

#include <iostream>
template<typename Type> class Base ;
template<typename Type> class Other : public Base<Type> ;
template<typename Type> class Derived : public Base<Type>

    public:
        Derived() std::cout<<"empty"<<std::endl;
        Derived(const Derived<Type>& x) std::cout<<"copy"<<std::endl;
        template<typename OtherType> explicit Derived(const Derived<OtherType>& x) std::cout<<"explicit"<<std::endl;
        template<typename OtherType> Derived(const Base<OtherType>& x) std::cout<<"implicit"<<std::endl;
;
int main()

    Other<int> other0;
    Other<double> other1;
    std::cout<<"1 = ";
    Derived<int> dint1;                     // <- empty
    std::cout<<"2 = ";
    Derived<int> dint2;                     // <- empty
    std::cout<<"3 = ";
    Derived<double> ddouble;                // <- empty
    std::cout<<"4 = ";
    Derived<double> ddouble1(ddouble);      // <- copy
    std::cout<<"5 = ";
    Derived<double> ddouble2(dint1);        // <- explicit
    std::cout<<"6 = ";
    ddouble = other0;                       // <- implicit
    std::cout<<"7 = ";
    ddouble = other1;                       // <- implicit
    std::cout<<"8 = ";
    ddouble = ddouble2;                     // <- nothing (normal : default assignment)
    std::cout<<"\n9 = ";
    ddouble = Derived<double>(dint1);       // <- explicit
    std::cout<<"10 = ";
    ddouble = dint2;                        // <- implicit : WHY ?!?!
    return 0;

最后一行让我担心。 C++标准可以吗?是g++的bug吗?

【问题讨论】:

为什么使用 Derived2&lt;OtherParameter1, OtherParameter2, OtherParameter&gt; 参数调用的构造函数更喜欢隐式构造函数而不是 explicit 构造函数,因为隐式构造函数的模板实例化比 explicit 构造函数更通用? 当前类和 // Derived2 可以不同... 如果我为隐式构造函数写:template&lt;class Other&gt; Derived2(const Base&lt;Other&gt;&amp; x) 我是否有保证Derived2&lt;OtherParameter1, OtherParameter2, OtherParameter&gt; 永远不会被隐式转换? 是的,当然它们可以(或通常会)不同,但与一般类型 T 相比,Derived2&lt;...&gt; 实例不会更好地匹配 Derived2&lt;...&gt; 参数吗? C++ 编译器不是为实例化寻找最不通用的模板吗? @ForEveR:使用explicit 构造函数,我认为它更像这样:liveworkspace.org/code/e48a3b5f1e670f785e68db4e67739b44 【参考方案1】:

由于您引用的每个构造函数都是模板化的类方法,因此会调用模板实例化和函数重载解析的规则。

如果您查看 C++11 标准的第 14.8.3 节,实际上第 1-3 段中有一些示例在一定程度上证明了您的问题。基本上,C++ 编译器将在一系列重载模板函数中寻找最佳匹配或“最不通用”的模板函数实例化(必要时添加类型转换)。在您的情况下,因为您已经显式创建了一个构造函数,该构造函数采用 Derived2 对象的备用实例化,与采用泛型类型 T 或甚至是 Base&lt;OtherParameter&gt; 参数。

UPDATE:显然,根据C++11标准中的12.3.1/2,

显式构造函数像非显式构造函数一样构造对象,但只有在 直接初始化语法 (8.5) 或显式使用强制类型转换 (5.2.9, 5.4)。

这意味着如果您不使用直接初始化语法来构造对象或选择强制转换,那么您将不能使用任何标记为explicit 的构造函数。这解释了您在测试 #9 和 #10 之间看到的令人费解的结果。

【讨论】:

问题在于,当使用复制初始化(而不是直接初始化)来构造另一个Derived2&lt;&gt; 实例时,仍然会调用隐式构造函数,即使这不是这里所需要的。 Online demo。这是 GCC 4.7.1 中的错误吗? @Jason 我添加了一个测试,结果与你说的似乎不同。 “问题”是您已将复制构造函数标记为 explicit,这似乎是 GCC 字面意思,这意味着除非您通过直接初始化显式调用复制构造函数,它不会调用它,并将回退到复制构造函数的替代候选者。在您的情况下,这是“隐式”模板复制构造函数。我不确定这是否是 GCC 中的错误。在我看来这是一个错误,但也许他们明确遵守的标准中有一些东西。 作为我上述评论的快速跟进,如果您从构造函数中删除 explicit 关键字,您会得到假设的行为:liveworkspace.org/code/4eda4a95dde4c345b1ffb543a4b0504f 好的,显然这种行为符合标准,至少根据 12.3.1/2。【参考方案2】:

您可以编写一个 trait 来报告一个类型是否是 Derived2&lt;&gt; 的特化:

template<typename T>
struct is_derived2 : std::false_type  ;

template<class P1, class P2, class P>
struct is_derived2<Derived2<P1, P2, P>> : std::true_type  ;

还有一个函数存根来提取Base&lt;P&gt;中的P

template<typename Parameter>
Parameter base_parameter(Base<Parameter> const&);

然后将你的隐式构造函数更改为:

template<
    class T,
    class = typename std::enable_if<
        !is_derived2<T>::value
        && std::is_base_of<
            Base<decltype(base_parameter(std::declval<T>()))>,
            T
        >::value
    >::type
>
Derived2(const T& x);

在线演示:http://liveworkspace.org/code/c43d656d60f85b8b9d55d8e3c4812e2b


更新:这是一个在线演示,将这些更改整合到您的“编辑 2”链接中:http://liveworkspace.org/code/3decc7e0658cfd182e2f56f7b6cafe61

【讨论】:

【参考方案3】:

好吧,也许我找到了一种解决方法,它只意味着添加了一个“假”构造函数:

#include <iostream>
#include <type_traits>
template<typename Type> class Base ;
template<typename Type> class Other : public Base<Type> ;
template<typename Type> class Derived : public Base<Type>

    public:
        Derived() std::cout<<"empty"<<std::endl;
        Derived(const Derived<Type>& x) std::cout<<"copy"<<std::endl;
        template<typename OtherType> explicit Derived(const Derived<OtherType>& x) std::cout<<"explicit"<<std::endl;
        template<typename Something> Derived(const Something& x) std::cout<<"implicit"<<std::endl;

    // Workaround
    public:
        template<template<typename> class Something, typename OtherType,
        class = typename std::enable_if< std::is_same< Something<OtherType>, Derived<OtherType> >::value>::type >
        Derived(const Something<OtherType>& x)
        std::cout<<"workaround (for example always false static assert here)"<<std::endl;
;
template<unsigned int Size> class Test ;

int main()

    Other<int> other0;
    Other<double> other1;
    Test<3> test;
    std::cout<<"1 = ";
    Derived<int> dint1;                     // <- empty
    std::cout<<"2 = ";
    Derived<int> dint2;                     // <- empty
    std::cout<<"3 = ";
    Derived<double> ddouble;                // <- empty
    std::cout<<"4 = ";
    Derived<double> ddouble1(ddouble);      // <- copy
    std::cout<<"5 = ";
    Derived<double> ddouble2(dint1);        // <- explicit
    std::cout<<"6 = ";
    ddouble = other0;                       // <- implicit
    std::cout<<"7 = ";
    ddouble = other1;                       // <- implicit
    std::cout<<"8 = ";
    ddouble = ddouble2;                     // <- nothing (normal : default assignment)
    std::cout<<"\n9 = ";
    ddouble = Derived<double>(dint1);       // <- explicit
    std::cout<<"10 = ";
    ddouble = dint2;                        // <- workaround
    std::cout<<"11 = ";
    ddouble = test;                         // <- implicit
    return 0;

@Everybody :你认为这是解决这个问题的好方法吗?

LWS : http://liveworkspace.org/code/f581356a7472c902b10ca486d648fafc

【讨论】:

我认为这不是一个好的解决方案,只是因为实际上不需要“解决方法”。如果您想要一种解决方法,那么您的问题并没有说清楚。 :-] 也许我错了,但我认为我可以放我想要的参数数量。仅当Something 具有与Derived 相同数量的参数时,编译器才会考虑解决方法。然后它将测试Something = Derived,在这种情况下,我有一个隐式构造函数,它将显示一个“假”静态断言。 难道你不想让重载决议做正确的事情而不是给出一个静态断言吗? 当然,一般情况下会这样。但是在我的实际应用程序中,我只想阻止这种转换并通过调用显式构造函数来强制用户知道他在做什么。

以上是关于隐式构造函数可用于从 Base 派生的所有类型(当前类型除外)?的主要内容,如果未能解决你的问题,请参考以下文章

C ++:从另一个构造函数隐式调用构造函数

explicit构造函数

explicit构造函数

如何使用 base 关键字从派生类初始化所有基类对象

C++一道题 跪求解答

base(C# 参考)