The perils of the accidental C++ conversion constructor

Raymond Chen

Raymond

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 });

6 comments

Comments are closed. Login to edit/delete your existing comments

  • Avatar
    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.

    • Avatar
      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:

      Buffer buffer1{3}; //length 1, contains 3
      Buffer buffer2(3); //length 3, contents unspecified

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

      • Avatar
        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):

        std::vector<int>  vec1{5}; // invokes vector(std::initializer_list)
        std::vector<int>  vec2(5); // invokes vector(size_t count)
        

        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.

  • Avatar
    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.)