开发者

Why is explicit allowed for default constructors and constructors with 2 or more (non-default) parameters?

开发者 https://www.devze.com 2023-01-30 20:21 出处:网络
I understand that constructors with one (non-default) parameter act like implicit convertors, which convert fro开发者_高级运维m that parameter type to the class type. However, explicit can be used to

I understand that constructors with one (non-default) parameter act like implicit convertors, which convert fro开发者_高级运维m 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?


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.


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.,


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.


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 
}


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

精彩评论

暂无评论...
验证码 换一张
取 消