Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compile time error if brace-closed list is the wrong size for class constructor

I'm trying to write a class based around mathematical vectors:

template <unsigned N> class Vector{
public:
    Vector() = default;
    Vector(std::initializer_list<double> li) { *this = li;}
    Vector& operator=(std::initializer_list<double>);

private:
    std::array<double, N> x = {}
}

template <unsigned N> inline Vector<N>& Vector<N>::operator=(std::initializer_list<double> li){
     if(N != li.size()) throw std::length_error("Attempt to initialise Vector with an initializer_list of different size.");
     std::copy(li.begin(), li.end(), x.begin());
     return *this;
}

I want to be able to write code like this;

Vector<3> a = {1,2,3};
a = {3,5,1};

It would be natural for a user to expect to write code like that, right? However I want compile-time errors to occur if I use the wrong-sized initializer list, much like std::array does.

 std::array<double, 3> a = {2,4,2,4} //compile time error
 Vector<3> a = {3,5,1,5} //run-time error as of right now

My first idea was to use std::array as the constructor/operator parameter so implicit conversions would occur and then the constructor would hijack, from std::array, the compile time errors. Except of course I could only write code like this:

Vector<3> a({2,3,2}); //fine
Vector<3> b = {2,4,2}; //error, requires two user-defined conversions (list -> array<double,3> -> Vector<3>) 

I thought maybe to use a Variadic member template:

template <typename... Args> Vector(Args... li): x({li...}){
    static_assert(sizeof...(li) == N);
}

It has to be typename... rather than double... because nontype parameters must be integral types. But then I run in to a narrowing conversion error

Vector<2> a = {3,2} //error: narrowing conversion of 'li#0' from 'int' to 'double' inside { } [-Wnarrowing]|
 //error: narrowing conversion of 'li#1' from 'int' to 'double' inside { } [-Wnarrowing]|

Presumably for violating [8.5.4]/7

A narrowing conversion is an implicit conversion

— 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

The parameters from expanding li... aren't constant expressions and hence produce the narrowing conversion error. As far as I'm aware it wouldn't even be possible to make function parameters as constant expressions (nor would it make much sense?). So I'm not sure how to carry on down that route. Obviously Vector<2> a = {2.,3.} works fine but this puts a burden on the user to remember only to supply floating-point literals.

like image 337
AntiElephant Avatar asked Nov 25 '15 20:11

AntiElephant


2 Answers

You can make your constructor a variadic template so that any condition can be used:

#include <array>
#include <cstddef>

template<typename T, std::size_t N>
class vector
{
public:
    vector(T&& value)
    : data{static_cast<T>(value)}
    {}
    template<typename U>
    vector(const vector<U,N>& v)
    {
      std::copy(begin(v.data), end(v.data),
                begin(data));
    }
    template<typename U>
    vector(const vector<U,N>& v)
    {
        std::copy(begin(v.data), end(v.data),
                  begin(data));
    }
    template<typename... U,
             typename = typename std::enable_if<sizeof...(U)-1>::type>
    vector(U&&... values)
    : data{static_cast<T>(values)...}
    {
        static_assert(sizeof...(values) == N, "wrong size");
    }
    std::array<T,N> data;
};

int main()
{
    vector<int, 3> v = {1,2,3};
    vector<double, 4> vv = {5,4,3,2};

    vv = {1,2,3,4};

    //vector<float, 3> vf = {1,2,3,4}; // fails to compile
    vector<float,3> vf = v;
}

Live example on coliru.

It gets you a custom error message, easily adaptable/extensible condition for failure, and gets rid of the "narrowing conversion" problem by effectively forwarding the initialization to the std::array initializer like you wanted to do in the first place. Oh, and you get assignment for free.

As @M.M mentions, this solution ruins copy construction, unfortunately. You can solve it by adding an enable_if on the variadic arguments "array" size as shown above. Of course you need to be careful to not ruin assignment/copy-construction and single-element vectors, which is remedied by adding two extra constructors for these special cases.

like image 156
rubenvb Avatar answered Nov 13 '22 21:11

rubenvb


This code seems to work, for both constructor and assignment-operator:

#include <array>

template<size_t N>
struct Vector
{
    Vector() = default;

    template<typename...Args>
    Vector(double d, Args... args)
    {
        static_assert(sizeof...(args) == N-1, "wrong args count");

        size_t idx = 0;
        auto vhelp = [&](double d) { x[idx++] = d; };
        vhelp(d);
        double tmp[] { (vhelp(args), 1.0)... };
    }

    Vector &operator=(Vector const &other) = default;

private:
    std::array<double, N> x = {};
};

int main()
{
    Vector<5> v = { 1,2,3,4,5 };
    v = { 3,4,5,6,7 };

    Vector<1> w = { 1,2 };  // error
}

The assignment operator works because the constructor is implicit, so v = bla attempts to convert bla to match the only definition of operator=.

I made the first argument double d instead of just using all variadic args, to avoid the issue where all-variadic-args constructor catches calls that were supposed to be copy-construction.

The line involving double tmp[] uses what I call the variadic template comma operator hack. This hack has many uses, but here it lets us avoid the narrowing-conversion issue that double tmp[] { args... }; has.

(TBH though, incorporating rubvenvb's idea and using double tmp[] { static_cast<double>(args)... }; would be simpler)

like image 39
M.M Avatar answered Nov 13 '22 23:11

M.M