std::reference_wrapper的隐式T&constructor是否会<T>使其使用起来很危险?
Does implicit T& constructor of std::reference_wrapper<T> make it dangerous to use?
boost::reference_wrapper<T>
有显式 T&
构造函数,而std::reference_wrapper<T>
有隐式构造函数。因此,在下面的代码中:
foo = bar;
如果foo
是boost::reference_wrapper
,代码将无法编译(这很好,因为reference_wrapper
不具有与实际引用相同的语义)。
如果foo
是std::reference_wrapper
,代码将"重新绑定"foo
对bar
的引用(而不是像人们可能错误地期望的那样赋值)。
这可能导致难以捉摸的bug…考虑下面的例子:
在版本1.0的某个假想库中:
void set_max(int& i, int a, int b) {
i = (a > b) ? a : b;
}
在新版本(1.1)中,set_max
被转换为模板以接受任何宽度的整数(或UDT),而无需更改接口:
template<typename T1, typename T2, typename T3>
void set_max(T1& i, T2 a, T3 b) {
i = (a > b) ? a : b;
}
最后,在一些使用库的应用中:
// i is a std::reference_wrapper<int> passed to this template function or class
set_max(i, 7, 11);
在这个例子中,库在不改变调用接口的情况下改变了set_max
的实现。这将无声地破坏任何传递std::reference_wrapper
的代码,因为参数将不再转换为int&
,而是"重新绑定"到悬空引用(a
或b
)。
我的问题:为什么标准委员会选择允许隐式转换(从T&
到std::reference_wrapper<T>
)而不是遵循boost
并使T&
构造函数显式?
编辑:(回应Jonathan Wakely提供的答案)…
最初的演示(在上面一节中)故意简洁地展示了一个细微的库更改是如何导致使用std::reference_wrapper
给应用程序引入bug的。
下一个演示是为了响应Jonathan Wakely的观点,展示真实世界中合法使用reference_wrapper
"通过接口传递引用"。
- 来自开发商/供应商A
类似于std::bind
,但假装它专门用于某些任务:
template<typename FuncType, typename ArgType>
struct MyDeferredFunctionCall
{
MyDeferredFunctionCall(FuncType _f, ArgType _a) : f(_f), a(_a) {}
template<typename T>
void operator()(T t) { f(a, t); }
FuncType f;
ArgType a;
};
- 来自开发商/供应商B
一个RunningMax
函子类。在这个假想库的1.0和1.1版本之间,RunningMax
的实现变得更加通用,而没有改变它的调用接口。对于本演示,旧的实现定义在名称空间lib_v1
中,而新的实现定义在lib_v2
中:
namespace lib_v1 {
struct RunningMax {
void operator()(int& curMax, int newVal) {
if ( newVal > curMax ) { curMax = newVal; }
}
};
}
namespace lib_v2 {
struct RunningMax {
template<typename T1, typename T2>
void operator()(T1& curMax, T2 newVal) {
if ( newVal > curMax ) { curMax = newVal; }
}
};
}
- 最后但并非最不重要的是,上述所有代码的最终用户:
某些开发人员使用来自供应商/开发人员A和B的代码来完成某些任务:
int main() {
int _i = 7;
auto i = std::ref(_i);
auto f = lib_v2::RunningMax{};
using MyDFC = MyDeferredFunctionCall<decltype(f), decltype(i)>;
MyDFC dfc = MyDFC(f, i);
dfc(11);
std::cout << "i=[" << _i << "]" << std::endl; // should be 11
}
最终用户按预期的方式使用
std::reference_wrapper
。单独,所有代码都没有错误或逻辑缺陷,并且与供应商B库的原始版本完美配合。
boost::reference_wrapper将在升级库时编译失败,而std::reference_wrapper会无声地引入一个可能在回归测试中捕获也可能不会捕获的错误。
跟踪这样的错误将是困难的,因为"重新绑定"不是
valgrind
等工具可以捕获的内存错误。此外,std::reference_wrapper
误用的实际站点应该在Vendor B的库代码中,而不是最终用户的库代码中。
底线: boost::reference_wrapper
通过显式声明其T&
构造函数似乎更安全,并且可以防止引入诸如此类的错误。在std::reference_wrapper
中删除显式构造函数限制的决定似乎是为了方便而牺牲了安全性,这在语言/库设计中应该很少发生。
这将无声地破坏任何传递给它
std::reference_wrapper
的代码,因为参数将不再转换为int&
,而是"重新绑定"到悬空引用(a或b)。
所以不要那样做。
: reference_wrapper
用于通过接口传递引用,否则会产生按值复制,而不是用于将传递给任意代码。也// i is a std::reference_wrapper<int> (perhaps b/c std::decay wasn't used)
decay
不会改变任何东西,它不会影响引用包装器
隐式转换 (T&
-> reference_wrapper<T>
)允许std::reference_wrapper<T>
, 而不允许 boost::reference_wrapper<T>
的原因在Nate Kohl提供的DR-689链接中得到了充分的解释。总结:
2007年,c++ 0x/c++ 11 库工作组 (LWG)提议对标准20.8.3.1 [refwrap.const]
部分进行更改#DR-689:
reference_wrapper的构造函数目前是显式的。这背后的主要动机是尊重的安全问题[DR-688]提案解决了这一问题。因此我们应该考虑放宽的要求由于隐式转换的请求保留重修的。
建议解析:从reference_wrapper的构造函数中移除explicit。
值得指出的是:
-
boost::reference_wrapper
没有以这种方式放松,也没有针对它的建议,这在boost::reference_wrapper
和std::reference_wrapper
的语义之间造成了不一致。 -
基于DR-689的措辞(特别是"请求不断出现"部分),似乎LWG只是简单地将此更改视为安全性和便利性之间的可接受权衡(与其boost对应物相比)。
-
目前尚不清楚LWG是否预期到其他潜在风险(如本页提供的示例所展示的风险),因为DR-689中提到的唯一风险是绑定到右值(如前一条目DR-688所描述和解决的)。
- 既然存在危险,为什么项目要使用-I include开关
- 未初始化的变量有什么危险
- EASTL矢量<向量<int>>连续的
- 将字符移出范围的危险
- 在C++的头文件中使用常量并在程序中询问其地址的任何潜在危险
- CRTP - 危险的内存访问?
- 危险指针的内存排序
- 在对象构造期间,将指向尚未构造的子对象的指针传递给另一个子对象的构造函数是否危险?
- 为什么 CWE 认为 rand() 具有潜在危险
- "this"关键字在C++中的实现限制,因为它与危险但功能示例有关
- cppcheck 抱怨危险地使用 c_str(). c_str() 返回的值在此调用后无效
- cpp 检查抱怨危险使用 c_str(). c_str() 返回的值在本次调用后无效,如何解决?
- 使用危险的幻数N
- 派生类的模板类作为函数的参数 - 危险?
- CUDA:来自不同翘曲但相同块的 2 个线程尝试写入相同的共享内存位置:危险?
- 聚合初始化的 C++17 扩展是否使大括号初始化变得危险?
- C - 创建矢量&lt; vector&lt; double&gt;&gt;矩阵具有分配而不是inizializ
- 从 int 到 longlong 的危险转换:没有警告
- C 字符串比较“祝您好运”&gt;“再见”
- 在模板中存储右值引用的危险