Search code examples
c++classconstructorinitializer-listtype-narrowing

Why is a narrowing conversion from int to float only needed if I brace-initialise an object?


I ran into what I think is a weird thing:

#include <vector>

int numqueues = 1;
std::vector<float> priorities{numqueues, 1.f };
//^^^ warning: narrowing conversion of numqueues from int to float

//std::vector<float> priorities(numqueues, 1.f );
//^^^ No warning or error. And it's not because it's parsed as a function declaration
// as I can call push_back in main.

int main()
{
    priorities.push_back(1);// No narrowing conversion needed
}

I've tried this using a couple of compilers, this won't compile.

Edit: It's been said that the initializer_list takes priority, and that looks to be the case, but I tried to mimic std::vector and I don't get the narrowing conversion error in this example:

#include <vector>
#include <iostream>
#include <initializer_list>

template <typename T>
class MyVector
{public:
    MyVector(size_t s, float f) {
        std::cout << "Called constructor\n";
    }
    MyVector(std::initializer_list<T> init)
    {
        std::cout << "Called initializer list constructor\n";
    }

};

int main()
{

    MyVector<float> foo{ size_t(3), 2.f };
}

I've done exactly the same thing, initialised it with size_t and float, just like in the other example, this one compiles fine.


Solution

  • In this declaration

    std::vector<float> priorities{numqueues, 1.f };
    

    the compiler uses the initializer list constructor.

    vector(initializer_list<T>, const Allocator& = Allocator());
    

    The narrowing conversion for initializer lists is prohibited.

    In this declaration

    std::vector<float> priorities(numqueues, 1.f );
    

    the compiler uses the constructor that specifies the number of elements and their initializer.

    vector(size_type n, const T& value, const Allocator& = Allocator());
    

    From the C++ 14 Standard (8.5.4 List-initialization)

    2 A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list or reference to possibly cv-qualified std::initializer_list for some type E, and either there are no other parameters or else all other parameters have default arguments (8.3.6). [ Note: Initializer-list constructors are favored over other constructors in list-initialization

    and (13.3.1.7 Initialization by list-initialization)

    1 When objects of non-aggregate class type T are list-initialized such that 8.5.4 specifies that overload resolution is performed according to the rules in this section, overload resolution selects the constructor in two phases:

    (1.1) — Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.

    (1.2) — If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the lements of the initializer list.

    Here is a demonstrative program

    #include <iostream>
    #include <initializer_list>
    
    struct A
    {
        A( std::initializer_list<float> )
        {
            std::cout << "A( std::initializer_list<float> )\n";
        }
        
        A( size_t, float )
        {
            std::cout << "A( size_t, float )\n";
        }
    };
    
    int main() 
    {
        A a1 { 1, 1.0f };
        A a2( 1, 1.0f );
        
        return 0;
    }
    

    The program output is

    A( std::initializer_list<float> )
    A( size_t, float )
    

    As for your appended question then (8.5.4 List-initialization)

    7 A narrowing conversion is an implicit conversion

    (7.3) — from an integer type or unscoped enumeration type to a floating-point type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted back to the original type, or

    So in this list initialization

    MyVector<float> foo{ size_t(3), 2.f };
    

    the constant expression size_t( 3 ) that fits into the type float is used.

    For example if in the above demonstrative program you will write

    size_t n = 1;
    
    A a1{ n, 1.0f };
    

    then the compiler should issue a message about narrowing conversion (at least the MS VS 2019 C++ compiler issues such an error message).