如何通过相当小的代码更改来修复未定义的行为
How to fix undefined behaviour with considerably small changes in code
在我们项目的核心中,我们有以下代码:
template<typename T>
T& get_factory_instance(bool reset = false)
{
static boost::scoped_ptr<T> factory_instance;
if (reset)
{
factory_instance.reset();
return *(T*)0;
}
if (!factory_instance)
{
factory_instance.reset(new T());
}
return *factory_instance;
}
如果reset
是true
,那么根据标准是UB。
只有当 return 值被忽略时才会调用参数为 true
的函数,因此不会访问内存。这是强制性的,当我们将此函数的调用添加到具有签名 void()
的单例函数列表时,不能从服务中调用函数,只能从库中调用。如果连接丢失,我们需要这种奇怪的技巧来清除连接到数据库的准备好的语句。
所以,基本上的问题是:如果我们不访问这段内存,它能触发吗?如果是的话,如果有可能的话,我们怎么可能在不重写依赖于这个函数的所有代码的情况下修复它呢?
当调用 T
构造函数时,它会构造准备好的语句和 open/use 到数据库的连接,因此,创建虚拟对象并不是一个好主意。
我们在输出库中使用参数 true
调用了该函数近 80 次。它于 2015 年推出。在大多数情况下,不带参数使用此函数的代码如下所示:
fields_t& fields = get_factory_instance<fields_t>();
提前致谢。
我知道这是遗留代码,您希望尽可能不对调用进行任何更改。此外,我假设对该函数的所有调用都是
fields_t& fields = get_factory_instance<fields_t>();
或
get_factory_instance<fields_t>(true);
换句话说,你永远不会通过
调用它
fields_t& fields = get_factory_instance<fields_t>(false);
尽管如您所见,这实际上不是什么大问题,因为使用以下解决方案会导致编译器错误,并且可以轻松修复。
您可以重构为:
template<typename T>
boost::scoped_ptr<T>& get_impl(){
static boost::scoped_ptr<T> instance;
return instance;
}
// or store the instance elsewhere
// and then...
template <typename T>
void get_factory_instance(bool) {
get_impl<T>().reset();
}
template<typename T>
T& get_factory_instance() {
auto& factory_instance = get_impl<T>();
if (!factory_instance)
{
factory_instance.reset(new T());
}
return *factory_instance;
}
或者,使用一些虚拟的默认静态 T
,您可以 return 作为参考,尽管那会相当浪费。
一般来说,当你没有 T
可以引用时,你不能安全地 return 一个 T&
,所以让“重置”调用调用一个 void
函数是我看到的唯一选择。
好的,您可以 return 一个仅在调用者请求时才转换为 T&
的代理对象,而不是添加 void
重载。对此持保留态度,它肯定比上面的更容易出错。我并不是真的推荐它,只是展示另一种可能性:
#include <iostream>
template <typename T>
struct maybe_ref_from_ptr { // first attempt of naming was optional_... but thats too misleading
T* ptr;
operator T& () { return *ptr; }
};
template <typename T>
maybe_ref_from_ptr<T> foo(bool reset = false) {
static T* p = nullptr;
if (reset ) {
if(p) delete p;
p = nullptr;
} else {
if (p == nullptr) p = new T(42);
}
return {p};
}
int main(){
int& i = foo<int>();
std::cout << i;
foo<int>(true);
int& j = foo<int>();
std::cout << j;
}
对于实际情况,必须调整代理以不简单地将原始指针存储到 T
,而是适当的智能指针。明显的缺点是现在避免 UB 的责任在调用者身上。虽然当指针无效时你可以在 operator T&
中 throw
...我承认我没有完全考虑到这一点,但我想你明白了。
在我们项目的核心中,我们有以下代码:
template<typename T>
T& get_factory_instance(bool reset = false)
{
static boost::scoped_ptr<T> factory_instance;
if (reset)
{
factory_instance.reset();
return *(T*)0;
}
if (!factory_instance)
{
factory_instance.reset(new T());
}
return *factory_instance;
}
如果reset
是true
,那么根据标准是UB。
只有当 return 值被忽略时才会调用参数为 true
的函数,因此不会访问内存。这是强制性的,当我们将此函数的调用添加到具有签名 void()
的单例函数列表时,不能从服务中调用函数,只能从库中调用。如果连接丢失,我们需要这种奇怪的技巧来清除连接到数据库的准备好的语句。
所以,基本上的问题是:如果我们不访问这段内存,它能触发吗?如果是的话,如果有可能的话,我们怎么可能在不重写依赖于这个函数的所有代码的情况下修复它呢?
当调用 T
构造函数时,它会构造准备好的语句和 open/use 到数据库的连接,因此,创建虚拟对象并不是一个好主意。
我们在输出库中使用参数 true
调用了该函数近 80 次。它于 2015 年推出。在大多数情况下,不带参数使用此函数的代码如下所示:
fields_t& fields = get_factory_instance<fields_t>();
提前致谢。
我知道这是遗留代码,您希望尽可能不对调用进行任何更改。此外,我假设对该函数的所有调用都是
fields_t& fields = get_factory_instance<fields_t>();
或
get_factory_instance<fields_t>(true);
换句话说,你永远不会通过
调用它fields_t& fields = get_factory_instance<fields_t>(false);
尽管如您所见,这实际上不是什么大问题,因为使用以下解决方案会导致编译器错误,并且可以轻松修复。
您可以重构为:
template<typename T>
boost::scoped_ptr<T>& get_impl(){
static boost::scoped_ptr<T> instance;
return instance;
}
// or store the instance elsewhere
// and then...
template <typename T>
void get_factory_instance(bool) {
get_impl<T>().reset();
}
template<typename T>
T& get_factory_instance() {
auto& factory_instance = get_impl<T>();
if (!factory_instance)
{
factory_instance.reset(new T());
}
return *factory_instance;
}
或者,使用一些虚拟的默认静态 T
,您可以 return 作为参考,尽管那会相当浪费。
一般来说,当你没有 T
可以引用时,你不能安全地 return 一个 T&
,所以让“重置”调用调用一个 void
函数是我看到的唯一选择。
好的,您可以 return 一个仅在调用者请求时才转换为 T&
的代理对象,而不是添加 void
重载。对此持保留态度,它肯定比上面的更容易出错。我并不是真的推荐它,只是展示另一种可能性:
#include <iostream>
template <typename T>
struct maybe_ref_from_ptr { // first attempt of naming was optional_... but thats too misleading
T* ptr;
operator T& () { return *ptr; }
};
template <typename T>
maybe_ref_from_ptr<T> foo(bool reset = false) {
static T* p = nullptr;
if (reset ) {
if(p) delete p;
p = nullptr;
} else {
if (p == nullptr) p = new T(42);
}
return {p};
}
int main(){
int& i = foo<int>();
std::cout << i;
foo<int>(true);
int& j = foo<int>();
std::cout << j;
}
对于实际情况,必须调整代理以不简单地将原始指针存储到 T
,而是适当的智能指针。明显的缺点是现在避免 UB 的责任在调用者身上。虽然当指针无效时你可以在 operator T&
中 throw
...我承认我没有完全考虑到这一点,但我想你明白了。