如果类构造函数的大括号闭合列表的大小错误,则编译时出错

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

本文关键字:错误 编译 出错 构造函数 如果 列表      更新时间:2023-10-16

我正试图编写一个基于数学向量的类:

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

我希望能够编写这样的代码;

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

用户期望编写这样的代码是很自然的,对吧?但是,如果使用大小错误的初始值设定项列表,我希望编译时出错,就像std::array一样。

 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

我的第一个想法是使用std::array作为构造函数/运算符参数,这样就会发生隐式转换,然后构造函数就会从std::array劫持编译时错误。当然,我只能写这样的代码:

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

我想也许可以使用Variadic成员模板:

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

它必须是typename...而不是double...,因为非类型参数必须是整型。但后来我遇到了一个缩小的转换错误

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]|

可能违反[8.5.4]/7

窄幅转换是一种隐式转换

--从整数类型或无范围枚举类型转换为浮点类型,除非源是常量表达式,并且转换后的实际值将适合目标类型,并在转换回原始类型或时生成原始值

扩展li...得到的参数不是常数表达式,因此会产生缩小的转换误差。据我所知,甚至不可能将函数参数作为常量表达式(这也没有多大意义?)。所以我不知道该怎么走下去。显然,Vector<2> a = {2.,3.}工作得很好,但这会给用户带来负担,让他们记住只提供浮点文字。

您可以使构造函数成为可变模板,以便可以使用任何条件:

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

coliru上的现场示例。

它为您提供了一个自定义的错误消息、易于调整/扩展的失败条件,并通过像您最初想要做的那样有效地将初始化转发到std::array初始化器来解决"缩小转换"问题。哦,你可以免费得到任务。

正如@M.M所提到的,不幸的是,这个解决方案破坏了复制构建。您可以通过在变量"数组"大小上添加enable_if来解决它,如上所示。当然,您需要注意不要破坏赋值/复制构造和单元素向量,这可以通过为这些特殊情况添加两个额外的构造函数来弥补。

这段代码似乎对构造函数和赋值运算符都有效:

#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
}

赋值运算符之所以有效,是因为构造函数是隐式的,所以v = bla尝试转换bla以匹配operator=的唯一定义。

我提出了第一个参数double d,而不是只使用所有可变参数,以避免所有可变参数构造函数捕获本应是复制构造的调用的问题。

涉及double tmp[]的一行使用了我所说的可变模板逗号运算符破解。这个黑客有很多用途,但在这里它让我们避免了double tmp[] { args... };的缩小转换问题。

(不过,TBH,结合rubvenvb的想法并使用double tmp[] { static_cast<double>(args)... };会更简单)