我了解具有一个(非默认)参数的构造函数的行为类似于隐式转换器,它将该参数类型转换为类类型。但是,explicit
可用于限定任何构造函数,没有参数的构造函数(默认构造函数)或具有 2 个或更多(非默认)参数的构造函数。
为什么在这些构造函数上允许显式?有没有什么例子可以用来防止某种隐式转换?
我了解具有一个(非默认)参数的构造函数的行为类似于隐式转换器,它将该参数类型转换为类类型。但是,explicit
可用于限定任何构造函数,没有参数的构造函数(默认构造函数)或具有 2 个或更多(非默认)参数的构造函数。
为什么在这些构造函数上允许显式?有没有什么例子可以用来防止某种隐式转换?
一个原因当然是因为它不疼。
需要它的一个原因是,如果您有第一个参数的默认参数。构造函数成为默认构造函数,但仍可用作转换构造函数
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 允许显式其他构造函数的决定根本不是一个坏主意。
根据高完整性 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
}
也许是为了支持维护。通过explicit
在多参数构造函数上使用,可以避免在向参数添加默认值时无意中引入隐式转换。虽然我不相信;相反,我认为只是在 C++ 中允许做很多事情,只是为了不让语言定义比现在更复杂。
也许最臭名昭著的情况是返回对非static
局部变量的引用。它需要额外的复杂规则来排除所有“无意义”的事情而不影响其他任何事情。所以它是允许的,如果你使用那个参考,就会产生 UB。
或者对于构造函数,您可以定义任意数量的默认构造函数,只要它们的签名不同,但是如果有多个,则很难默认调用它们中的任何一个。:-)
一个更好的问题可能是,为什么explicit
转换运算符也不允许?
好吧,在 C++0x 中。所以没有充分的理由不这样做。不允许explicit
转换操作员的实际原因可能与疏忽一样平淡无奇,或者一开始就很难被explicit
采用,或者委员会时间的简单优先级,等等。
干杯&hth.,
class_t::operator=
显式默认构造函数的一个原因是,当接受类型为U
and的对象的重载时,避免在赋值右侧进行容易出错的隐式转换std::is_same_v<U, class_t> == false
。例如,如果我们有一个将移动赋值运算符重载为类似 的东西,而应该可以转换为的东西,则类似的赋值class_t_instance = {}
可能会导致我们不想要的结果。可以在考虑默认构造(观察类型对象)的赋值的情况下编写令人困惑的赋值,但实际上程序员正在“擦除”,因为这个赋值与默认构造函数是隐式的一样。看一下 an 的玩具实现:observable<T>
observable<T>::operator=(U&&)
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
}