显式关键字(explicit keyword)是什么意思?

3638

explicit关键字在C++中的含义是什么?


225
我想指出的是,自从 C++11 版本以来,explicit 不再只能用于构造函数,还可以用于类型转换运算符。假设你有一个类 BigInt,它有一个将对象转换为 int 的类型转换运算符,并且还有一个将对象显式转换为 std::string 的类型转换运算符(出于某种原因)。你可以这样写 int i = myBigInt;,但在将对象显式转换为 std::string 时,则需要进行显式转换(最好使用 static_cast)。 - chris
2
“Can't explicit” 也可以指赋值吗?(即 int x(5); - Eitan Myron
@chris 在隐式转换的声明中可以使用一个显式关键字。 - curiousguy
@chris 你是不是指这样做:std::string s = static_cast<std::string>(myBigInt)?如果可以的话,能否进一步解释一下你的第一个评论呢?非常感谢! - Milan
2
@Milan,是的,就是这样。如果你正在寻找更多信息,这个答案会更正式地写出来。请注意,bool在这方面是特殊的。那些答案和搜索“显式转换运算符”将带领你找到更多关于这个特性的文章,并且比评论链更适合。 - chris
显示剩余2条评论
11个回答

4143

编译器允许进行一次隐式转换以解决函数参数。这意味着编译器可以使用可调用单个参数的构造函数将一种类型转换为另一种类型,以获取参数的正确类型。

这里是一个具有可以用于隐式转换的构造函数的示例类:

class Foo
{
private:
  int m_foo;

public:
  // single parameter constructor, can be used as an implicit conversion
  Foo (int foo) : m_foo (foo) {}

  int GetFoo () { return m_foo; }
};

这是一个简单的函数,它接受一个Foo对象:

void DoBar (Foo foo)
{
  int i = foo.GetFoo ();
}

这里是调用DoBar函数的地方:

int main ()
{
  DoBar (42);
}
参数不是一个Foo对象,而是一个int。 但是,存在一个以int为参数的构造函数可以将参数转换为正确的类型。
编译器允许针对每个参数执行一次此操作。
在构造函数前缀添加explicit关键字可防止编译器使用该构造函数进行隐式转换。 将其添加到上面的类中将在函数调用DoBar (42)处创建编译器错误。 现在需要显式调用转换 DoBar (Foo (42))。
你可能希望这样做是为了避免意外构建可以隐藏错误的情况。
虚构的例子:
你有一个MyString类,它具有构造函数,可构造给定大小的字符串。 你有一个函数print(const MyString&)(以及重载print(char *string)),并调用print(3)(当你实际上打算调用print("3")) 。 你期望它打印“3”,但它打印长度为3的空字符串。

250
不错的写作,你可能想提到带有默认参数的多参数构造函数也可以作为单参数构造函数,例如,Object(const char * name = NULL,int otype = 0)。 - maccullt
579
我认为还应该提到,一开始应该考虑将单参数构造函数设为显式(基本上自动),只有在设计需要隐式转换时才删除explicit关键字。我认为构造函数默认应该是显式的,使用“implicit”关键字使它们可以作为隐式转换工作。但事实并非如此。 - Michael Burr
12
@thecoshman说:你不会声明一个参数explicit,而是声明一个构造函数explicit。但是,没错:你的类型为Foo的参数必须显式地构造,不能仅仅通过将它们的构造函数参数插入到函数中就隐式地构造它们。 - Christian Severin
98
提醒一下,在你的例子中调用“print(3)”时,函数应该是“print(const MyString&)”。这里“const”是必须的,因为3会被转换为一个临时的“MyString”对象,而临时对象不能绑定到非const的引用上(这是C++中的又一个陷阱)。 - Larry
57
为了完整起见,我要补充说明,除了参数转换之外,这里的“explicit”关键字还将防止使用复制构造函数的赋值形式(例如,Foo myFoo = 42;),并要求使用显式形式 Foo myFoo = Foo(42); 或 Foo myFoo(42);。 - Arbalest
显示剩余2条评论

1333

假设你有一个类String

class String {
public:
    String(int n); // allocate n bytes to the String object
    String(const char *p); // initializes object with char *p
};

现在,如果您尝试:

String mystring = 'x';

字符'x'将被隐式转换为int,然后将调用String(int)构造函数。但是,这不是用户可能想要的结果。因此,为了防止这种情况,我们应将构造函数定义为explicit

class String {
public:
    explicit String (int n); //allocate n bytes
    String(const char *p); // initialize sobject with string p
};

18
值得注意的是,C++0x 的新的通用初始化规则将使 String s = {0}; 变得不合法,而不是像 String s = 0; 那样尝试使用空指针调用另一个构造函数。 - Johannes Schaub - litb
11
尽管这是一个旧问题,但似乎值得指出一些事情(或者让有经验的人纠正我)。通过将int形式或两个构造函数中的任意一个声明为“explicit”,如果您在本意是使用 String mystring("x") 时错误地使用了 String mystring('x') ,您仍将遇到同样的错误,不是吗?此外,从上面的评论中,我们可以看出通过将构造函数的 int 形式设置为“explicit”可以改进使用 String s = 0 的行为,即使用 String s = {0}。但是,除了了解构造函数的优先级之外,您如何知道 String s{0} 的意图(即如何发现错误)? - Arbalest
1
为什么 String mystring = 'x'; 会被转换成 int? - InQusitive
14
'x'被视为整数,因为char数据类型只是一个1字节的整数 - DavidRR
11
你的例子有问题,它只适用于拷贝初始化(使用“=”),但不适用于直接初始化(不使用“=”):如果你写String mystring('x');,编译器仍然会调用String(int)构造函数而不生成错误,正如@Arbalest指出的那样。explicit关键字是为了防止直接初始化和函数解析中发生的隐式转换。对你的例子来说,更好的解决方案是简单地重载构造函数:String(char c); - Géry Ogam
我有点困惑。即使使用了 explicit 关键字,String mystring = 'x' 仍然会调用具有 int 类型的相同构造函数,是吗? - xyf

200

在C++中,只有一个必需参数的构造函数被视为隐式转换函数。它将参数类型转换为类类型。这是否是一件好事取决于构造函数的语义。

例如,如果你有一个字符串类,它有一个名为String(const char* s)的构造函数,那可能正是你想要的。你可以将一个const char*传递给期望一个String的函数,编译器会自动为你构造一个临时的String对象。

另一方面,如果你有一个缓冲区类,它的构造函数Buffer(int size)接收缓冲区大小(以字节为单位),你可能不希望编译器悄悄地将int转换成Buffer。为了防止这种情况发生,你需要使用explicit关键字来声明构造函数:

class Buffer { explicit Buffer(int size); ... }

那样,

void useBuffer(Buffer& buf);
useBuffer(4);

变成了编译时错误。如果您想传递一个临时的 Buffer 对象,您必须明确地这样做:

变成了编译时错误。如果您想传递一个临时的Buffer对象,则必须显式地进行传递:

useBuffer(Buffer(4));

总之,如果你的单参数构造函数将参数转换为类对象,那么你可能不想使用explicit关键字。但是,如果你有一个仅仅需要一个参数的构造函数,你应该声明它为explicit,以防止编译器意外地进行意想不到的转换。


10
useBuffer 函数需要一个左值作为其参数,useBuffer(Buffer(4)) 也不能工作,因为它不是左值。将其改为接受 const Buffer&Buffer&& 或只接受 Buffer 将使其工作。 - pqnet

67

关键字explicit的使用场景

  • 伴随不能将第一个(且仅有的)参数隐式转换为类型X的类X构造函数。

C++ [class.conv.ctor]

1) 没有显式指定“explicit”函数说明符的构造函数,会将其参数类型转换为该类的类型。这种构造函数称为转换构造函数。

2) 明确指定了“explicit”函数说明符的构造函数像非明确构造函数一样构造对象,但只能在直接初始化语法(8.5)或强制类型转换(5.2.9, 5.4)中使用。 默认构造函数可以是显式构造函数;这种构造函数可用于执行默认初始化或值初始化(8.5)。

  • 或者作为只考虑直接初始化和显式转换的转换函数。

C++ [class.conv.fct]

2) 转换函数可能是显式的(7.1.2),此时它只被视为用户定义的转换函数用于直接初始化(8.5)。 否则,用户自定义的转换不限于在赋值和初始化中使用。

概述

显式转换函数和构造函数只能用于显式转换(直接初始化或显式强制类型转换),而非明确的构造函数和转换函数可以用于隐式和显式的转换。

/*
                                 explicit conversion          implicit conversion

 explicit constructor                    yes                          no

 constructor                             yes                          yes

 explicit conversion function            yes                          no

 conversion function                     yes                          yes

*/

使用结构体X、Y、Z和函数foo、bar、baz的示例:

让我们看一下一个小型结构体和函数设置,以了解explicit和非explicit转换之间的区别。

struct Z { };

struct X { 
  explicit X(int a); // X can be constructed from int explicitly
  explicit operator Z (); // X can be converted to Z explicitly
};

struct Y{
  Y(int a); // int can be implicitly converted to Y
  operator Z (); // Y can be implicitly converted to Z
};

void foo(X x) { }
void bar(Y y) { }
void baz(Z z) { }

构造函数的示例:

函数参数的转换:

foo(2);                     // error: no implicit conversion int to X possible
foo(X(2));                  // OK: direct initialization: explicit conversion
foo(static_cast<X>(2));     // OK: explicit conversion

bar(2);                     // OK: implicit conversion via Y(int) 
bar(Y(2));                  // OK: direct initialization
bar(static_cast<Y>(2));     // OK: explicit conversion

对象初始化:

X x2 = 2;                   // error: no implicit conversion int to X possible
X x3(2);                    // OK: direct initialization
X x4 = X(2);                // OK: direct initialization
X x5 = static_cast<X>(2);   // OK: explicit conversion 

Y y2 = 2;                   // OK: implicit conversion via Y(int)
Y y3(2);                    // OK: direct initialization
Y y4 = Y(2);                // OK: direct initialization
Y y5 = static_cast<Y>(2);   // OK: explicit conversion

转换函数示例:

X x1{ 0 };
Y y1{ 0 };

函数参数的转换:

baz(x1);                    // error: X not implicitly convertible to Z
baz(Z(x1));                 // OK: explicit initialization
baz(static_cast<Z>(x1));    // OK: explicit conversion

baz(y1);                    // OK: implicit conversion via Y::operator Z()
baz(Z(y1));                 // OK: direct initialization
baz(static_cast<Z>(y1));    // OK: explicit conversion

对象初始化:

Z z1 = x1;                  // error: X not implicitly convertible to Z
Z z2(x1);                   // OK: explicit initialization
Z z3 = Z(x1);               // OK: explicit initialization
Z z4 = static_cast<Z>(x1);  // OK: explicit conversion

Z z1 = y1;                  // OK: implicit conversion via Y::operator Z()
Z z2(y1);                   // OK: direct initialization
Z z3 = Z(y1);               // OK: direct initialization
Z z4 = static_cast<Z>(y1);  // OK: explicit conversion

为什么要使用explicit转换函数或构造函数?

转换构造函数和非显式转换函数可能会引入歧义。

考虑一个结构体V,可以转换为int,一个从V隐式构造的结构体U和一个针对Ubool重载的函数f

struct V {
  operator bool() const { return true; }
};

struct U { U(V) { } };

void f(U) { }
void f(bool) {  }

如果传递一个类型为V的对象,对f的调用是不明确的。

V x;
f(x);  // error: call of overloaded 'f(V&)' is ambiguous

编译器不知道是否要使用 U 的构造函数还是转换函数将 V 对象转换为传递给 f 函数的类型。

如果 U 的构造函数或者 V 的转换函数有一个是 explicit 的,那么就不会有二义性,因为只会考虑非显式转换。如果两个都是显式的,那么使用类型为 V 的对象调用 f 就必须使用显式转换或强制转换操作。

转换构造函数和非显式转换函数可能导致意外的行为。

考虑一个打印一些向量的函数:

void print_intvector(std::vector<int> const &v) { for (int x : v) std::cout << x << '\n'; }
如果vector的大小构造函数不是显式的,那么就可以这样调用函数:
print_intvector(3);

这个调用应该期望什么?一个包含3的行,还是三个包含0的行?(第二种情况是发生的。)

在类接口中使用explicit关键字可以强制用户明确所需的转换。

正如Bjarne Stroustrup在《C++程序设计语言》第四版35.2.1页1011所说,关于为什么std::duration不能从纯数字隐式构造:

如果你知道你的意思,请明确地表达出来。


1
如果你知道你的意思,就要明确地表达出来。这是一句很棒的名言。再提醒我一下,为什么会有自动化存在呢? :-) - stu
@stu自动存在的目的是让C++更容易编写,但更难阅读 :( - Jeremy Friesner
这很棒,直到你成为继承该系统的人。 - stu

59

这个回答主要讲解对象的创建,包括使用和不使用显式构造函数的情况,因为其他回答中没有涉及到这方面内容。

考虑下面这个没有显式构造函数的类:

class Foo
{
public:
    Foo(int x) : m_x(x)
    {
    }

private:
    int m_x;
};

类 Foo 的对象可以通过以下 2 种方式创建:

Foo bar1(10);

Foo bar2 = 20;

根据实现方式不同,使用第二种类Foo的实例化方法可能会令程序员感到困惑,而这也不是程序员所期望的。将explicit关键字作为构造函数的前缀加入会在Foo bar2 = 20;处生成编译器错误。

通常情况下,将单个参数的构造函数声明为explicit是一个好习惯,除非你的实现特别禁止它。

还要注意,带有

  • 所有参数默认值,或者
  • 从第二个参数开始的所有参数都有默认值

的构造函数都可以用作单个参数构造函数。因此,你可能还想将这些构造函数也声明为explicit

当你创建一个函数对象时,你故意希望将单个参数构造函数设置为explicit,例如在查看答案中声明的'add_x'结构体。在这种情况下,像add_x add30 = 30;这样创建对象可能是有意义的。

这里是关于显式构造函数的一篇很好的文章。


50

explicit关键字将转换构造函数变为非转换构造函数,因此代码更少出错。


38
explicit关键字可用于强制要求显式调用构造函数。
class C {
public:
    explicit C() =default;
};

int main() {
    C c;
    return 0;
}

explicit关键字放在构造函数C()之前,告诉编译器只允许显式调用此构造函数。

explicit关键字也可用于用户自定义类型转换运算符:

class C{
public:
    explicit inline operator bool() const {
        return true;
    }
};

int main() {
    C c;
    bool b = static_cast<bool>(c);
    return 0;
}

在这里,explicit关键字强制要求只有显式转换是有效的,因此在此情况下,bool b = c;将是无效的转换。在这样的情况下,explicit关键字可以帮助程序员避免隐含的、意外的转换。这种用法已经被标准化在C++11中。


18
第一个示例中的C c();并不是你想象的意思:它声明了一个名为c的函数,该函数不带参数并返回C类的一个实例。 - 6502
2
explicit operator bool() 是 C++11 版本的安全布尔类型,可以在条件检查中隐式使用(只能在条件检查中使用,据我所知)。在您的第二个示例中,这行代码也可以在 main() 中使用:if (c) { std::cout << "'c' is valid." << std:: endl; }。除此之外,它不能在没有显式转换的情况下使用。 - Justin Time - Reinstate Monica
需要显式调用构造函数。 - curiousguy
@JustinTime 这是一个愚蠢的、破损的 safe bool 版本。显式隐式转换的整个概念都是荒谬的。 - curiousguy
@curiousguy 确实。这似乎有点像一个临时解决方案,更多的是为了易于记忆(可能希望它被频繁使用),而不是遵循英语逻辑,并设计为不与以前的安全布尔实现完全不兼容(因此如果您将其替换,您不太可能破坏任何东西)。在我看来,至少是这样。 - Justin Time - Reinstate Monica
我修改了 C c(); 的错误,并在编译器探索器中进行了尝试,发现将默认构造函数设置为 explicit 实际上并没有起到任何作用。即使已经进行了代码更正,这个答案仍然是错误的。 - JDługosz

32

Cpp Reference永远是有帮助的!!! 有关explicit specifier的详细信息可以在这里找到。你可能还需要查看implicit conversionscopy-initialization

快速浏览

explicit specifier指定了构造函数或转换函数(自C++11以来)不允许隐式转换或复制初始化。

示例如下:

struct A
{
    A(int) { }      // converting constructor
    A(int, int) { } // converting constructor (C++11)
    operator bool() const { return true; }
};

struct B
{
    explicit B(int) { }
    explicit B(int, int) { }
    explicit operator bool() const { return true; }
};

int main()
{
    A a1 = 1;      // OK: copy-initialization selects A::A(int)
    A a2(2);       // OK: direct-initialization selects A::A(int)
    A a3 {4, 5};   // OK: direct-list-initialization selects A::A(int, int)
    A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)
    A a5 = (A)1;   // OK: explicit cast performs static_cast
    if (a1) cout << "true" << endl; // OK: A::operator bool()
    bool na1 = a1; // OK: copy-initialization selects A::operator bool()
    bool na2 = static_cast<bool>(a1); // OK: static_cast performs direct-initialization

//  B b1 = 1;      // error: copy-initialization does not consider B::B(int)
    B b2(2);       // OK: direct-initialization selects B::B(int)
    B b3 {4, 5};   // OK: direct-list-initialization selects B::B(int, int)
//  B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
    B b5 = (B)1;   // OK: explicit cast performs static_cast
    if (b5) cout << "true" << endl; // OK: B::operator bool()
//  bool nb1 = b2; // error: copy-initialization does not consider B::operator bool()
    bool nb2 = static_cast<bool>(b2); // OK: static_cast performs direct-initialization
}

1
explicit operator bool()if是一个特殊情况。无法使用用户定义的Boolexplicit operator Bool()和名为If的函数来复制它。 - curiousguy

24

在编写代码时,将你的单参数构造函数制作为最佳编程实践(包括那些具有默认值arg2arg3等的构造函数),正如先前所述。

另一个类的良好实践是将复制构造和赋值操作设为私有(即禁用它),除非你确实需要实现它。这样做可以避免在使用C++为您默认创建的方法时产生指针的副本。另一种方法是从boost::noncopyable派生。


26
这篇文章是在2009年写的。今天你不再声明它们为私有,而是使用"= delete"来表达。 - v010dya
1
第一个句子中是否缺少了"explicit"这个词?应该写成"如前所述_明确"吗? - Will

9
构造函数附加隐式转换。要抑制此隐式转换,需要声明一个带有显式参数的构造函数。
在C++11中,您还可以使用关键字“operator type()”来指定这样的操作符http://en.cppreference.com/w/cpp/language/explicit。通过这样的规定,您可以使用显式转换和对象的直接初始化来使用运算符。
附注:当使用用户定义的转换(通过构造函数和类型转换运算符)时,只允许使用一级隐式转换。但是,您可以将此转换与其他语言转换相结合。
  • 上升整数等级(char到int,float到double);
  • 标准转换(int到double);
  • 将对象指针转换为基类和void *;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接