如何避免初始化列表构造器优先匹配?最佳实践探讨
我创建了一个最小可复现示例:
#include <iostream> #include <initializer_list> #include <algorithm> #include <stdexcept> class Vector { private: size_t sz; double* elem; public: Vector(size_t s) : sz{s}, elem{new double[s]} {} Vector(std::initializer_list<double> lst) : sz{lst.size()}, elem{new double[lst.size()]} { std::copy(lst.begin(), lst.end(), elem); } size_t size() const {return sz;} double& operator[](size_t i) { if (i >= sz) // 注:size_t是无符号类型,i<0永远为false throw std::out_of_range("Vector::operator[](size_t): index out of range"); return elem[i]; } }; int main() { Vector v1{3}; Vector v2{v1.size()}; std::cout << v2[0] << '\n'; std::cout << v2[1] << '\n'; }
编译时出现警告:
problem.cpp:25:22: warning: narrowing conversion of ‘v1.Vector::size()’ from ‘size_t’ {aka ‘long unsigned int’} to ‘double’
运行时触发错误:
terminate called after throwing an instance of 'std::out_of_range' what(): Vector::operator[](size_t): index out of range
问题根源是初始化列表构造器的优先级高于其他构造器:Vector v1{3}和Vector v2{v1.size()}都匹配了std::initializer_list<double>构造器,把整数/无符号整数窄转成double后,构造出了仅含1个元素的Vector,调用v2[1]自然越界。
我已定位问题,但现有参考未给出完整解决方案,想咨询:
- 使用
()初始化是否可行?但这会违背统一初始化的初衷; - 是否要将部分构造器设为
explicit? - 或是有其他标准规避方式?
推荐解决方案
1. 将初始化列表构造器标记为explicit
这是最标准的解决方式,直接切断隐式匹配初始化列表构造器的路径。标记后,只有当你明确传入初始化列表(比如{1.0, 2.0, 3.0})时,才会触发这个构造器;而{3}或{v1.size()}会优先匹配size_t版本的构造器。
修改后的构造器代码:
explicit Vector(std::initializer_list<double> lst) : sz{lst.size()}, elem{new double[lst.size()]} { std::copy(lst.begin(), lst.end(), elem); }
此时Vector v1{3}会正确调用Vector(size_t s),构造出含3个元素的Vector;Vector v2{v1.size()}也会正常匹配size_t构造器,不会再出现窄转警告和越界错误。
2. 补充整数类型的重载构造器(可选优化)
因为size_t是无符号类型,直接用{3}(int类型)初始化时,虽然编译器会隐式转换,但可以添加一个针对int的重载构造器,明确处理整数输入,避免潜在的类型转换问题:
Vector(int s) : Vector(static_cast<size_t>(s)) {}
配合explicit的初始化列表构造器,{3}会直接匹配这个int版本的构造器,再委托给size_t版本,逻辑更清晰。
3. 妥协方案:使用()初始化
如果不想修改类定义,临时可以用()代替{}来触发size_t构造器:
Vector v1(3); Vector v2(v1.size());
但这确实违背了统一初始化的初衷,而且如果后续有人再次误用{},问题会重现,因此不推荐作为长期解决方案。
内容的提问来源于stack exchange,提问作者dandan13

