【问题标题】:why implicit conversion is harmful in C++为什么隐式转换在 C++ 中是有害的
【发布时间】:2010-02-27 03:14:11
【问题描述】:

我知道关键字explicit 可以用来防止隐式转换。

例如

Foo {

 public:
 explicit Foo(int i) {}
}

我的问题是,在什么情况下应该禁止隐式转换?为什么隐式转换有害?

【问题讨论】:

    标签: c++


    【解决方案1】:

    如果您希望出现编译错误,请使用 explicit

    explicit 仅适用于构造函数中有一个参数(或许多第一个参数是唯一没有默认值的参数)的情况。

    在程序员可能错误地构造对象的任何时候,您都想使用explicit 关键字,认为它可能会做一些实际上并没有做的事情。

    这是一个例子:

    class MyString
    {
    public:
        MyString(int size)
            : size(size)
        {
        }
    
         //... other stuff
    
        int size;
    };
    

    使用以下代码,您可以这样做:

    int age = 29;
    //...
    //Lots of code
    //...
    //Pretend at this point the programmer forgot the type of x and thought string
    str s = x;
    

    但是调用者可能打算将“3”存储在 MyString 变量中,而不是 3。最好得到一个编译错误,以便用户可以首先调用 x 变量上的 itoa 或其他一些转换函数。

    对上述代码产生编译错误的新代码:

    class MyString
    {
    public:
        explicit MyString(int size)
            : size(size)
        {
        }
    
         //... other stuff
    
        int size;
    };
    

    编译错误总是比错误好,因为它们立即可见,您可以更正。

    【讨论】:

    • explicit 也适用于当你有很多参数的构造函数并且除了第一个之外的所有参数都有默认值,例如:显式 RealWorldObject(int a, int b = 15, char c = 'a' );
    • 显式也可以添加到stop一个编译错误。想想一个指向T 的智能指针,它有一个转换operator T *() 和一个来自T * 的复制构造函数。当你有像(a == 0) 这样的表达式时,这是否意味着在a 上调用强制转换运算符并在两个原始指针上使用==,或者它应该从0 构造一个智能指针并在两个原始指针上使用==智能指针?使构造函数显式解决了这种歧义。尽管出于各种原因,首先没有转换运算符会更好。例如shared_ptr 有显式复制,没有转换。
    【解决方案2】:

    它引入了意想不到的临时工

    struct Bar
    {
        Bar();      // default constructor
        Bar( int ); // value constructor with implicit conversion
    };
    
    void func( const Bar& );
    
    Bar b;
    b = 1; // expands to b.operator=( Bar( 1 ));
    func( 10 ); // expands to func( Bar( 10 ));
    

    【讨论】:

    • b=1 在这里等于 b(1),但是为什么有害呢?
    • 因为简单的语法隐藏了潜在的昂贵操作——临时的构造、复制、临时的销毁。
    • 因为出乎意料。语法看起来像您将 b 设置为 1,但它创建了一个新对象。它隐藏了真正发生的事情。 blogs.msdn.com/oldnewthing/archive/2006/05/24/605974.aspx
    • @Nikolai:实际上,这不是问题。 C++ 中的所有内容都隐藏了昂贵的操作,例如重载的operator+。使用它的真正原因是当它做了一些编写它的人可能没有预料到的事情时;通过使用完整的语法,希望它们会查找或 IDE 会查找它们
    • 我原来不是这么说的吗?
    【解决方案3】:

    一个真实世界的例子:

    class VersionNumber
    {
    public:
        VersionNumber(int major, int minor, int patch = 0, char letter = '\0') : mMajor(major), mMinor(minor), mPatch(patch), mLetter(letter) {}
        explicit VersionNumber(uint32 encoded_version) { memcpy(&mLetter, &encoded_version, 4); }
        uint32 Encode() const { int ret; memcpy(&ret, &mLetter, 4); return ret; }
    
    protected:
        char mLetter;
        uint8 mPatch;
        uint8 mMinor;
        uint8 mMajor;
    };
    

    VersionNumber v = 10; 几乎可以肯定是一个错误,所以explicit 关键字要求程序员输入VersionNumber v(10); 并且 - 如果他或她使用的是一个不错的 IDE - 他们会通过 IntelliSense 弹出窗口注意到它需要一个encoded_version.

    【讨论】:

      【解决方案4】:

      大多数情况下,隐式转换是一个问题,当它允许代码编译(并且可能做一些奇怪的事情)时,你做了一些你不打算做的事情,并且宁愿代码没有编译,而是一些转换允许编译代码并做一些奇怪的事情。

      例如,iostream 转换为void *。如果您有点累并输入类似:std::cout << std::cout; 它实际上会编译 - 并产生一些毫无价值的结果 - 通常类似于 8 位或 16 位十六进制数(在 32 位系统上为 8 位,16 位在 64 位系统上)。

      同时,我觉得有必要指出,很多人似乎对任何形式的隐式转换都产生了一种近乎反射性的厌恶。有些类对隐式转换有意义。例如,代理类允许转换为另一种特定类型。对于代理而言,永远不会出乎意料地转换为该类型,因为它只是一个代理——也就是说,你可以(并且应该)认为它完全等同于它作为代理的类型——除了当然,要做好事,它必须针对某种特定情况实施一些特殊行为。

      例如,几年前我编写了一个bounded<T> 类,它表示始终保持在指定范围内的(整数)类型。其他拒绝分配超出指定范围的值,它的行为与底层整数类型完全相同。它(主要)通过提供到 int 的隐式转换来做到这一点。几乎你用它做的任何事情,它都会像一个 int 一样。本质上,唯一的例外是当您为其赋值时——如果该值超出范围,它将引发异常。

      【讨论】:

        【解决方案5】:

        这对有经验的人无害。可能对初学者或调试他人代码的新手有害。

        【讨论】:

        • 但另一方面,除非隐式转换是直观的,否则必须输入更多字符以便未来的维护者(甚至您自己)准确地看到正在发生的事情是否会造成伤害?
        【解决方案6】:

        “有害”是一个强有力的声明。 “不是不经思考就可以使用的东西”是一个很好的说法。大部分 C++ 都是这样的(尽管有些人可能认为 C++ 的某些部分是有害的......)

        无论如何,隐式转换最糟糕的部分是,它不仅会在您不期望的时候发生,而且除非我弄错了,否则它可以链接...只要类型 Foo 之间存在隐式转换路径并键入 Bar,编译器会找到它,并沿该路径进行转换 - 这可能会产生许多您没有预料到的副作用。

        如果它获得的唯一好处是不必输入几个字符,那就不值得了。直言不讳意味着你知道实际发生的事情并且不会被咬。

        【讨论】:

          【解决方案7】:

          要扩展布赖恩的答案,请考虑一下:

          class MyString
          {
          public:
              MyString(int size)
                  : size(size)
              {
              }
          
              // ...
          };
          

          这实际上允许这段代码编译:

          MyString mystr;
          // ...
          if (mystr == 5)
              // ... do something
          

          编译器没有 operator== 来将 MyString 与 int 进行比较,但它知道如何从 int 中生成 MyString,因此它会像这样查看 if 语句:

          if (mystr == MyString(5))
          

          这非常具有误导性,因为它看起来像是在将字符串与数字进行比较。事实上,假设 MyString(int) 构造函数创建一个空字符串,这种类型的比较可能永远不会有用。如果将构造函数标记为显式,则禁用这种类型的转换。所以要小心隐式转换——注意它允许的所有语句类型。

          【讨论】:

            【解决方案8】:

            我使用显式作为转换(单参数或等效)构造函数的默认选择。当我在一个类和另一个类之间进行转换时,我宁愿让编译器立即告诉我,并在此时做出决定是否转换合适,或者改为更改我的设计或实现以完全消除转换的需要。

            Harmful 是隐式转换的一个稍强的词。它对最初的实施有害,但对应用程序的维护有害。隐式转换允许编译器静默更改类型,尤其是在另一个函数调用的参数中 - 例如自动将 int 转换为其他对象类型。如果您不小心将一个 int 传递给该参数,编译器将“有帮助地”为您创建临时变量,当事情不正常时让您感到困惑。当然我们都可以说“哦,我永远不会犯那个错误”,但只需要一次调试几个小时,就会开始认为让编译器告诉你这些转换是个好主意。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-08-16
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多