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

发布于 2024-10-08 01:15:53 字数 176 浏览 10 评论 0 原文

我知道带有一个(非默认)参数的构造函数就像隐式转换器,它从该参数类型转换为类类型。但是,explicit 可用于限定任何构造函数、不带参数的构造函数(默认构造函数)或具有 2 个或更多参数(非默认)的构造函数。

为什么这些构造函数允许显式调用?有没有任何例子可以证明这对于防止某种隐式转换很有用?

I understand that constructors with one (non-default) parameter act like implicit convertors, which convert from that parameter type to the class type. However, explicit can be used to qualify any constructor, those with no parameters (default constructor) or those with 2 or more (non-default) parameters.

Why is explicit allowed on these constructors? Is there any example where this is useful to prevent implicit conversion of some sort?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

岛徒 2024-10-15 01:15:53

原因之一当然是因为它不疼。

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

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 s1{10, 15}; // fine

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

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

One reason certainly is because it doesn't hurt.

One reason where it's needed is, if you have default arguments for the first parameter. The constructor becomes a default constructor, but can still be used as converting constructor

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

C++0x makes actual use of it for multi parameter constructors. In C++0x, an initializer list can be used to initialize a class object. The philosophy is

  • if you use = { ... }, then you initialize the object with a sort of "compound value" that conceptually represents the abstract value of the object, and that you want to have converted to the type.

  • if you use a { ... } initializer, you directly call the constructors of the object, not necessarily wanting to specify a conversion.

Consider this example

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 s1{10, 15}; // fine

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

In this way, C++0x shows that the decision of C++03, to allow explicit on other constructors, wasn't a bad idea at all.

我们只是彼此的过ke 2024-10-15 01:15:53

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

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

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

更好的问题也许是,为什么转换运算符不允许显式使用?

好吧,在 C++0x 中。所以没有充分的理由不这么做。不允许在转换运算符上使用显式的实际原因可能与监督一样平淡无奇,或者是在首先采用显式时遇到的困难,或者是委员会时间的简单优先级,或者其他什么。

干杯&呵呵,

Perhaps it was to support maintainance. By using explicit on multi-argument constructors one might avoid inadvertently introducing implicit conversions when adding defaults to arguments. Although I don't believe that; instead, I think it's just that lots of things are allowed in C++ simply to not make the language definition more complex than it already it is.

Perhaps the most infamous case is returning a reference to non-static local variable. It would need additional complex rules to rule out all the "meaningless" things without affecting anything else. So it's just allowed, yielding UB if you use that reference.

Or for constructors, you're allowed to define any number of default constructors as long as their signatures differ, but with more than one it's rather difficult to have any of them invoked by default. :-)

A better question is perhaps, why is explicit not also allowed on conversion operators?

Well it will be, in C++0x. So there was no good reason why not. The actual reason for not allowing explicit on conversion operators might be as prosaic as oversight, or the struggle to get explicit adopted in the first place, or simple prioritization of the committee's time, or whatever.

Cheers & hth.,

微凉徒眸意 2024-10-15 01:15:53

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

根据 各种 sources,当应用于不能用恰好一个调用的构造函数时,它根本没有任何效果争论。

It's probably just a convenience; there's no reason to dis-allow it, so why make life difficult for code generators, etc? If you checked, then code generation routines would have to have an extra step verifying how many parameters the constructor being generated has.

According to various sources, it has no effect at all when applied to constructors that cannot be called with exactly one argument.

灰色世界里的红玫瑰 2024-10-15 01:15:53

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

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 
}

According to the High Integrity C++ Coding Standard you should declare all sinlge parameter constructor as explicit for avoiding an incidentally usage in type conversions. In the case it is a multiple argument constructor suppose you have a constructor that accepts multiple parametres each one has a default value, converting the constructor in some kind of default constructor and also a conversion constructor:

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 
}
不奢求什么 2024-10-15 01:15:53

显式默认构造函数的原因之一是,当存在接受类型为 class_t::operator= 重载时,避免在赋值右侧容易出错的隐式转换>U 和 std::is_same_v; ==假。例如,如果我们有一个 observable 将移动赋值运算符重载为 < code>observable::operator=(U&&),而 U 应该可以转换为 T。令人困惑的赋值可以通过默认构造的T(观察类型对象)的赋值来编写,但实际上程序员正在“擦除”observable因为如果默认构造函数是隐式的,则此分配与 class_t_instance = class_t_instance{} 相同。看一下 observable 的玩具实现:

#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
}

One reason to explicit a default constructor is to avoid an error-prone implicit conversion on the right hand side of an assignment when there is an overload to class_t::operator= that accepts an object with type U and std::is_same_v<U, class_t> == false. An assignment like class_t_instance = {} can lead us to an undesirable result if we have, for example, an observable<T> that overloads the move assignment operator to something like observable<T>::operator=(U&&), while U should be convertible to T. The confusing assignment could be written with an assignment of a default constructed T (observed type object) in mind, but in reality the programmer is "erasing" the observable<T> because this assignment is the same as class_t_instance = class_t_instance{} if the default constructor is implicit. Take a look at a toy implementation of an 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
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文