【问题标题】:How to fix undefined behaviour with considerably small changes in code如何通过对代码进行相当小的更改来修复未定义的行为
【发布时间】:2021-06-30 18:56:55
【问题描述】:

在我们项目的核心中,我们有以下代码:

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

如果resettrue,则根据标准它是UB。

这个带有参数true的函数只有在返回值被忽略时才会被调用,因此不会访问内存。这是强制性的,当我们将此函数的调用添加到签名为void() 的函数的单例列表中时,不能从服务调用函数,只能从库调用函数。如果连接丢失,我们需要这种奇怪的 hack 来清除连接到数据库的准备好的语句。

所以,基本上问题是:如果我们不访问此内存,它会触发吗?如果是的话,如果有可能的话,我们怎么可能在不重写所有依赖于这个函数的代码的情况下修复它?

T 构造函数被调用时,它会构造预处理语句并打开/使用与数据库的连接,因此,创建虚拟对象并不是一个好主意。

在 out 库中,我们有近 80 次使用参数 true 调用此函数。它于 2015 年推出。在大多数情况下使用此函数而不带参数的代码如下所示:

fields_t& fields = get_factory_instance<fields_t>();

提前致谢。

【问题讨论】:

  • 您可以返回对虚拟static 实例的引用。您将返回对标记值的非常量引用,这不是很好,但至少不会是 UB。
  • reset 标志的用途是什么?不应该导致工厂实例被重新创建?该标志应该首先解决什么问题?
  • @ForEveR 在我看来get_factory_instance 是一个根本性的破坏设计,您的团队应该咬紧牙关,正确地修复它(重构它)。 80 个呼叫站点很重要,但也不是压倒性的。
  • @Martin.Martinsson> 这完全是题外话,但我发现它之前的 OMG 和这里的 LOL 很烦人。我们都必须处理多年前某天离开公司的人编写的遗留代码。如果没有人对一个严肃的问题发笑,这已经很烦人了。请:)
  • @Martin.Martinsson,那么,如果你不更多地使用这个内存,会有什么问题呢?伙计,这显然是题外话。我知道,这是导致 UB 的错误代码,但已经完成了。这不是我的代码通过,所以,你的话是错误的。

标签: c++ reference undefined-behavior


【解决方案1】:

我知道这是遗留代码,如果可能的话,您不希望对调用进行任何更改。此外,我假设对函数的所有调用都是

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,您可以返回一个引用,尽管这会相当浪费。

一般来说,当您没有可引用的T 时,您无法安全地返回T&amp;,因此我看到的唯一选择是进行“重置”调用调用void 函数。


好吧,与其添加void 重载,不如返回一个代理对象,该对象仅在调用者请求时才转换为T&amp;。对此持保留态度,它肯定比上述更容易出错。我不是真的推荐它,它只是为了展示另一种可能性:

#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&amp;throw...我承认我没有完全考虑到这一点,但我想你明白了。

【讨论】:

  • 这显然是一个解决方案。我检查了我所有的代码库: grep -r get_factory_instance --include=*.cpp * | grep false 并没有显示。非常感谢你。在测试新代码后,我会接受这个答案。
  • 这不是真的吗,如果我们不使用默认参数并且我们确保没有错误的调用,所以我们只返回 void,或者 T&,我们不会在任何地方得到 UB?
  • @ForEveR 你指的是我的代码中的bool reset = true 吗?没关系,我可以删除它。一旦您确定该函数仅在没有参数或true 的情况下被调用,该函数甚至可能只是void get_factory_instance(bool) { get_impl&lt;T&gt;().reset(); },尽管我可能会留下assert(reset == true) 以防止有人意外调用get_factory_instance(false)
  • 断言不是在执行时触发,而不是在编译时触发?
  • @ForEveR 运行时不幸。再想一想,我想它不需要。我只是有点偏执。例如,我正在考虑有人在没有注意到的情况下将函数指针指向错误的函数,但不同的返回类型已经阻止了这种情况。我从答案中删除了它...
猜你喜欢
  • 2020-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多