January 15th, 2021

The perils of the accidental C++ conversion constructor

Consider this class:

class Buffer
{
public:
  Buffer(size_t capacity);
  Buffer(std::initializer_list<int> values);
};

You can create an uninitialized buffer with a particular capacity, or you can create an initialized buffer.

The one-parameter constructor also serves as a conversion constructor, resulting in the following:

Buffer buffer(24); // create a buffer of size 24
Buffer buffer({ 1, 3, 5 }); // create an initialized 3-byte buffer

Okay, those don’t look too bad. But you also get this:

Buffer buffer = 24; // um...
Buffer buffer = { 1, 3, 5 };

These are equivalent to the first two versions, but you have to admit that the = 24 version looks really weird.

You also get this:

extern void Send(Buffer const& b);
Send('c'); // um...

This totally compiles, but it doesn’t send the character 'c', which is what it looks like. Instead, it creates an uninitialized buffer of size 0x63 = 99 and sends it.

If this is not what you intended, then you would be well-served to use the explicit keyword to prevent a constructor from being used as conversion constructions.

class Buffer
{
public:
  explicit Buffer(size_t capacity);
  Buffer(std::initializer_list<int> values);
};

I made the first constructor explicit, since I don’t want you to pass an integer where a buffer is expected. However, I left the initializer list as a valid conversion constructor because it seems reasonable to let someone write

Send({ 1, 2, 3 });
Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

6 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮

    I strongly suggest we have a new “C++” tag dedicated to the documentation of the hijinks of the C++ language.

  • Jim LyonMicrosoft employee

    For this reason, I habitually put “explicit” on every constructor, except copy constructors. (If you put explicit on a copy constructor, your object becomes non-copyable, defeating the point of the constructor.)

  • Theo Belaire

    This exact issue with a very similar class caused me so much grief in the past.

    Even worse I think

    Buffer buffer = { 3 };

    will try and use the first one or something? This exact issue is what killed my interest in C++ (after a week or two of debugging segfaults). Just so unhappy when I figured out it was my fault for not declaring the constructor explicit. I haven’t taken a C++ job since then.

    • Jacob Manaker

      Nope! According to https://en.cppreference.com/w/cpp/language/list_initialization, an initializer_list constructor is preferred to a non-initializer_list constructor (see the sixth bullet under "Explanation"). This fits with the C++ heuristic that you want to use curly brackets when listing out the values in your object, but parentheses when specifying parameters to a (constructor) method. Thus: <code>

      I love this language, but that might be the Stockholm Syndrome speaking.

      Read more
      • gast128

        I stumbled upon this problem years ago since std::vector has a similar interface (i.e. one with count and one with an initializer_list):
        <code>
        The curly brace initialization is unfortunately not a drop in replacement. std::vector should have added and support for an unambiguous initialization for the 'count' overload; e.g. using an extra enumerate argument.

        I like C++ but the language has sharp edges.

        Read more
      • Alex Cohn

        Another unfortunate issue with std::vector is that there is no reserve constructor.