c++implicit-conversionstdinitializerlist

C++ Braced Initialization Not Triggering Expected Conversion Operator


I'm working with a C++ class that includes both a conversion operator and a constructor that takes a std::initializer_list.
According to Scott Meyers in "Effective Modern C++," Item 7, compilers strongly prefer to match braced initializers with constructors that accept std::initializer_lists. This preference is so pronounced that it can override what would normally be copy and move construction, instead opting for the std::initializer_list constructor.
However, in my tests, the conversion operator isn't being invoked as expected. I am guessing implicit copy operator is invoked instead. Below is a simplified version of the code from Scott Meyers' book:

#include <iostream>
#include <initializer_list>
#include <utility>

class Widget {
public:

    Widget(int i, bool b) {
        std::cout << "Widget(int, bool) called with values: " << i << ", " << b << std::endl;
    }

    Widget(int i, double d) {
        std::cout << "Widget(int, double) called with values: " << i << ", " << d << std::endl;
    }

    Widget(std::initializer_list<long double> il) {
        std::cout << "Widget(std::initializer_list<long double>) called with values: ";
        for (auto value : il) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
    }
    operator float() const {
        std::cout << "Conversion operator to float called" << std::endl;
        // Dummy conversion logic; in a real class this would convert internal state to float
        return 0.0f;
    }
};

int main() {
    Widget w1(10, true);
    Widget w2(10, 5.0);

    Widget w3{ 10, 5.0 };
    Widget w4(10, 5.0);

    Widget w5(w4);
    Widget w6{ w4 }; // uses braces, calls
                     // std::initializer_list ctor
                     // (w4 converts to float, and float
                     // converts to long double)

    Widget w7(std::move(w4));
    Widget w8{std::move(w4) }; // uses braces, calls
                              // std::initializer_list ctor
                              // (for same reason as w6)
}

The comments for w6 and w8 are copied from the book.

output:

Widget(int, bool) called with values: 10, 1

Widget(int, double) called with values: 10, 5

Widget(std::initializer_list<long double>) called with values: 10 5 

Widget(int, double) called with values: 10, 5

Solution

  • It's a bug in MSVC which uses the copy constructor in

    Widget w6{w4};
    

    and the move constructor in

    Widget w8{std::move(w4)};
    

    gcc, clang and icx all agree on the output you expect:

    Widget(int, bool) called with values: 10, 1
    Widget(int, double) called with values: 10, 5
    Widget(std::initializer_list<long double>) called with values: 10 5 
    Widget(int, double) called with values: 10, 5
    Conversion operator to float called
    Widget(std::initializer_list<long double>) called with values: 0 
    Conversion operator to float called
    Widget(std::initializer_list<long double>) called with values: 0
    

    Demo

    I also wrote a bugreport that you can vote for if you'd like this fixed.