为啥显式允许默认构造函数和具有 2 个或更多(非默认)参数的构造函数?

Posted

技术标签:

【中文标题】为啥显式允许默认构造函数和具有 2 个或更多(非默认)参数的构造函数?【英文标题】:Why is explicit allowed for default constructors and constructors with 2 or more (non-default) parameters?为什么显式允许默认构造函数和具有 2 个或更多(非默认)参数的构造函数? 【发布时间】:2011-05-26 21:53:52 【问题描述】:

我了解具有一个(非默认)参数的构造函数的行为类似于隐式转换器,它将该参数类型转换为类类型。但是,explicit 可用于限定任何构造函数,没有参数的构造函数(默认构造函数)或具有 2 个或更多(非默认)参数的构造函数。

为什么在这些构造函数上允许显式?有没有什么例子可以用来防止某种隐式转换?

【问题讨论】:

Falmarri:如果不被阻止,是否有一些示例在默认构造函数或 >=2 参数构造函数上很有用? 见阿德里安的回答。为什么要阻止它? Falmarri:如果我的查询暗示我“不想”在这样的构造函数上使用它,我很抱歉。相反,我很想知道“为什么”它是允许的。 【参考方案1】:

其中一个原因当然是因为它不疼。

需要它的一个原因是,如果您有第一个参数的默认参数。构造函数成为默认构造函数,但仍可用作转换构造函数

struct A 
  explicit A(int = 0); // added it to a default constructor
;

C++0x 将它实际用于多参数构造函数。在 C++0x 中,初始化列表可用于初始化类对象。哲学是

如果您使用= ... ,那么您使用一种“复合值”来初始化该对象,该复合值在概念上表示对象的抽象值,并且您希望转换为类型。

如果使用 ... 初始化器,则直接调用对象的构造函数,不一定要指定转换。

考虑这个例子

struct String 
    // this is a non-converting constructor
    explicit String(int initialLength, int capacity);
;

struct Address 
    // converting constructor
    Address(string name, string street, string city);
;

String s =  10, 15 ; // error!
String s110, 15; // fine

Address a =  "litb", "nerdsway", "frankfurt" ; // fine

通过这种方式,C++0x 表明 C++03 允许在其他构造函数上显式的决定根本不是一个坏主意。

【讨论】:

因此,多参数构造函数上的explicit 给出的结果类似于复制构造函数上的explicit @James 类似于任何单参数可调用构造函数上的显式 :) 但列表初始化的不同之处在于它仍然考虑显式构造函数。当它被选中时,会出现一个诊断。与 T t = v 不同,后者只是忽略显式构造函数,可能更喜欢非显式构造函数,委员会认为这是一件坏事。【参考方案2】:

也许是为了支持维护。通过在多参数构造函数上使用explicit,可以避免在向参数添加默认值时无意中引入隐式转换。虽然我不相信;相反,我认为在 C++ 中允许做很多事情只是为了不让语言定义变得比现在更复杂。

也许最臭名昭著的情况是返回对非static 局部变量的引用。它需要额外的复杂规则来排除所有“无意义”的事情而不影响其他任何事情。所以这是允许的,如果你使用那个引用,就会产生 UB。

或者对于构造函数,您可以定义任意数量的默认构造函数,只要它们的签名不同,但是如果有多个,则很难默认调用其中的任何一个。 :-)

一个更好的问题可能是,为什么 explicit 也不允许在转换运算符上使用?

好吧,在 C++0x 中。所以没有充分的理由不这样做。不允许 explicit 在转换运算符上使用的实际原因可能与疏忽一样平淡无奇,或者首先要让 explicit 被采用,或者委员会时间的简单优先级,等等。

干杯,

【讨论】:

【参考方案3】:

这可能只是为了方便;没有理由dis - 允许它,那么为什么要让代码生成器等的生活变得困难呢?如果你检查了,那么代码生成例程必须有一个额外的步骤来验证正在生成的构造函数有多少参数。

根据varioussources,当应用于不能只用一个参数调用的构造函数时,它根本没有效果。

【讨论】:

re“没有一个参数”,你的意思是,当应用于不能用一个参数调用的构造函数时没有效果。有区别。 ;-) 细微的区别,但没关系 :) 已修复。【参考方案4】:

根据高完整性 C++ 编码标准 您应该将所有单参数构造函数声明为 显式 以避免在类型转换。在它是多参数构造函数的情况下,假设您有一个接受多个参数的构造函数,每个参数都有一个默认值,将构造函数转换为某种默认构造函数以及转换构造函数:

class C  
    public: 
    C( const C& );   // ok copy 
    constructor C(); // ok default constructor 
    C( int, int ); // ok more than one non-default argument 

    explicit C( int ); // prefer 
    C( double ); // avoid 
    C( float f, int i=0 ); // avoid, implicit conversion constructor 
    C( int i=0, float f=0.0 ); // avoid, default constructor, but 
                               // also a conversion constructor 
; 
void bar( C const & ); 
void foo() 
 
    bar( 10 );  // compile error must be 'bar( C( 10 ) )' 
    bar( 0.0 ); // implicit conversion to C 

【讨论】:

【参考方案5】:

显式默认构造函数的一个原因是,当class_t::operator= 接受类型为Ustd::is_same_v<U, class_t> == false 的对象的重载时,避免赋值右侧容易出错的隐式转换。如果我们有一个observable<T> 将移动赋值运算符重载为observable<T>::operator=(U&&) 之类的东西,那么class_t_instance = 之类的赋值可能会导致我们不希望的结果,而U 应该可以转换为T。可以用默认构造的T(观察到的类型对象)的赋值来编写令人困惑的赋值,但实际上程序员正在“擦除”observable<T>,因为如果默认情况下这个赋值与class_t_instance = class_t_instance相同构造函数是隐式的。看看observable<T> 的玩具实现:

#include <boost/signals2/signal.hpp>
#include <iostream>
#include <type_traits>
#include <utility>
 
template<typename T>
struct observable 
    using observed_t = T;
    
    //With an implicit default constructor we can assign `` instead
    //of the explicit version `observable<int>`, but I consider this
    //an error-prone assignment because the programmer can believe
    //that he/she is defining a default constructed
    //`observable<T>::observed_t` but in reality the left hand side
    //observable will be "erased", which means that all observers will
    //be removed.
    explicit observable() = default;
    
    explicit observable(observed_t o) : _observed(std::move(o)) 
 
    observable(observable&& rhs) = default;
    observable& operator=(observable&& rhs) = default;
 
    template<typename U>
    std::enable_if_t<
        !std::is_same_v<std::remove_reference_t<U>, observable>,
        observable&>
    operator=(U&& rhs) 
        _observed = std::forward<U>(rhs);
        _after_change(_observed);
        return *this;
    
 
    template<typename F>
    auto after_change(F&& f)
     return _after_change.connect(std::forward<F>(f)); 
    
    const observed_t& observed() const noexcept
     return _observed; 
private:
    observed_t _observed;
    boost::signals2::signal<void(T)> _after_change;
;

int main()
    observable<int> o;
    o.after_change([](auto v) std::cout << "changed to " << v << std::endl; ); //[1]
    o = 5;
 
    //We're not allowed to do the assignment `o = `. The programmer
    //should be explicit if he/she desires to "clean" the observable.
    o = observable<int>;
    
    o = 10; //the above reaction [1] is not called;
    
    //outputs:
    //changed to 5

【讨论】:

以上是关于为啥显式允许默认构造函数和具有 2 个或更多(非默认)参数的构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

C++17 中的显式默认构造函数

为啥统一初始化没有显式构造函数[重复]

如果类具有参数化构造函数,为啥Java不提供默认构造函数? [复制]

显式复制构造函数和统一初始化

为啥我的显式构造函数会为我的转换运算符创建这种歧义?

为啥为非泛型方法或构造函数提供显式类型参数会编译?