【问题标题】:C++ "smart pointer" template that auto-converts to bare pointer but can't be explicitly deleted自动转换为裸指针但不能显式删除的 C++“智能指针”模板
【发布时间】:2010-07-22 18:21:09
【问题描述】:

我在一个非常庞大的遗留 C++ 代码库中工作,该代码库将保持无名。作为遗留代码库,它到处传递原始指针。但是我们正在逐渐尝试对其进行现代化改造,因此也有一些智能指针模板。这些智能指针(与 Boost 的 scoped_ptr 不同)具有到原始指针的隐式转换,因此您可以将其中一个传递到采用原始指针的例程中,而无需编写 .get()。这样做的一个很大的缺点是,您也可能不小心在 delete 语句中使用了一个,然后您就有了一个双重释放的错误,这可能是一个很难追踪的问题。

有没有办法修改模板,使其仍然隐式转换为原始指针,但如果在删除语句中使用会导致编译错误?像这样:

#include <my_scoped_ptr>

struct A {};
extern void f(A*);

struct B
{
    scoped_ptr<A> a;

    B();
    ~B();
};

B::B()
    : a(new A)
{
    f(a); // this should compile
}

B::~B()
{
    delete a; // this should NOT compile
}

【问题讨论】:

    标签: c++ templates smart-pointers


    【解决方案1】:

    标准说

    操作数应具有指针类型,或具有单个转换函数(12.3.2)到指针类型的类类型。如果操作数具有类类型,则通过调用上述转换函数将操作数转换为指针类型,并在本节的其余部分使用转换后的操作数代替原始操作数。

    您可以 (ab) 通过声明转换函数的 const 版本来使用重载解析的缺失。在足以使它不再与delete 一起工作的一致编译器上:

    struct A {
      operator int*() { return 0; }
      operator int*() const { return 0; }
    };
    
    int main() {
      A a;
      int *p = a; // works
      delete a; // doesn't work
    }
    

    结果如下

    [js@HOST2 cpp]$ clang++ main1.cpp
    main1.cpp:9:3: error: ambiguous conversion of delete expression of type 'A' to a pointer
      delete a; // doesn't work
      ^      ~
    main1.cpp:2:3: note: candidate function            
      operator int*() { return 0; }
      ^
    main1.cpp:3:3: note: candidate function             
      operator int*() const { return 0; }
      ^
    1 error generated.
    

    在这方面不太符合标准的编译器(EDG/Comeau、GCC)上,您可以将转换函数设为模板。 delete 不期望 特定的 类型,所以这会起作用:

    template<typename T>
    operator T*() { return /* ... */ }
    

    但是,这样做的缺点是您的智能指针现在可以转换为 any 指针类型。虽然实际的转换仍然经过类型检查,但这并不排除预先进行转换,而是在很久以后给出编译时错误。可悲的是,C++03 中的转换函数似乎无法使用 SFINAE :) 另一种方法是从另一个函数返回私有嵌套类型指针

    struct A {
      operator int*() { return 0; }
    
    private:
      struct nested { };
      operator nested*() { return 0; }
    };
    

    现在唯一的问题是转换为void*,在这种情况下,两个转换函数同样可行。 @Luther 建议的解决方法是从另一个转换函数返回一个函数指针类型,它适用于 GCC 和 Comeau,并摆脱了 void* 问题,同时在通常的转换路径上没有其他问题,不像模板解决方案

    struct A {
      operator int*() { return 0; }
    
    private:
      typedef void fty();
      operator fty*() { return 0; }
    };
    

    请注意,这些变通方法仅适用于不符合要求的编译器。

    【讨论】:

    • 你会那样做的,不是吗。
    • g++ 未定,4.4 不编译,4.5 可以。如果我将第二次转换的类型更改为函数指针类型,那么 g++ 会正确并停止在“删除 a”处。
    • 不应该是int* operator();const int* operator() const;吗?
    • @Ben 转换函数具有operator 关键字之后的类型。如果他们尝试将smart_ptr&lt;T*&gt; const 转换为T*,则应用const 两次将失败。所以这两个转换函数都返回T*(毕竟,一个const指针与指向const的指针不同。smart_ptr&lt;T const*&gt;当然会转换为T const*,这与原始指针的作用相同)。
    • 是的,我的语法不好。我在想this 的 const-ness 是如何归咎于成员的,但它仍然是T* const 而不是const T*,所以返回类型仍然是T*,就像你展示的那样。
    【解决方案2】:

    没有办法阻止一个而不是另一个。任何可以隐式转换为函数调用指针的地方,都可以隐式转换为删除表达式。

    最好的办法是删除转换功能。您的情况正是用户定义的转换运算符很危险且不应经常使用的原因。


    I'm wrong.:(

    【讨论】:

    • 查看我对“In silicio”的回复。我知道反对隐式转换的论点,但在这种情况下没有它们根本不切实际。
    • @Zack:那么您就不必删除它们。这是您的选择:删除隐式转换并使用显式 get() 函数,或保留隐式转换并希望不调用 delete。
    • 我担心你是对的,但我会把这个问题留一天左右,以防有人想出一些聪明的东西。
    • @Zack:删除隐式转换可能很乏味,但一旦完成,您就会知道它们将来不会造成问题。对我来说,这值得做一些无脑的苦差事。
    • @Zack:当然会很糟糕。遗憾的是,他的解决方案不适用于 VC++。
    【解决方案3】:

    您可以使用Boost 提出的技术,但我担心的是您允许从智能指针隐式转换为原始指针,这通常是不受欢迎的。此外,用户可以通过-&gt; 操作符获得的指针调用delete,所以你真的无法阻止一个坚定的白痴绕过你想出的任何机制。

    你真的应该只实现一个get() 方法而不是提供operator T*(),这样至少对delete smartptr 的调用不会编译。非白痴应该能够弄清楚这可能是有原因的。

    是的,输入LegacyFunc(smartptr.get()) 比输入LegacyFunc(smartptr) 需要更多的工作,但前者是首选,因为它明确并防止发生意外转换,例如delete smartptr

    如果你有这样的功能怎么办:

     void LegacyOwnPointer(SomeType* ptr);
    

    函数将指针存储在哪里?这会搞砸智能指针,因为现在它不知道其他东西拥有原始指针。

    不管怎样,你都有一些工作要做。智能指针类似于原始指针,但它们并不相同,因此您不能只查找并替换 T* 的所有实例并将其替换为 my_scoped_ptr&lt;T&gt; 并期望它像以前一样工作。

    【讨论】:

    • 不管它是否被反对,如果不隐式转换为原始指针,试图将智能指针硬塞到这个遗留代码库中是没有希望的。您需要提供原始指针的地方太多了。 - 只有当我可以修改指向的类时,您的建议才会有所帮助,而我通常不能。我需要一种完全在智能指针类中工作的技术。
    • 我真的不认为你欣赏在一个 300 万行、15 年历史的代码库中工作的现实。如果我从智能指针模板(已经被大量使用)中取出隐式转换,我将不得不进行 数千 次 .get() 调用,我的同事会私刑我。
    【解决方案4】:

    对此并没有考虑太多,但是...您能否为 operator delete 提供一个重载,该重载对于您的模板类的实例是强类型的,这样当包含代码时编译会失败?如果这是在您的头文件中,则应防止调用中的隐式转换以支持调用您的重载。

    操作员删除(my_scoped_ptr) { //... 不可编译的代码放在这里 }

    如果这是一个愚蠢的想法,我们深表歉意。

    【讨论】:

    • 不,会为“删除指向智能指针的指针”的表达式调用,但不会为导致问题的“删除智能指针”的表达式调用。
    【解决方案5】:

    我可以看到您不想在哪里大量应用 .get()。你有没有考虑过用更小的替换删除?

    struct A
    {
        friend static void Delete( A* p) { delete p; }
    
    private:
        ~A(){}
    };
    
    struct B
    {
    };
    
    int main() 
       { 
    
        delete new B();  //ok
    
        Delete( new A ); //ok
    
        delete new A; //compiler error
    
        return (0); 
        } 
    

    【讨论】:

    • 这只有在我可以修改指向的类时才有帮助,而我通常不能这样做。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-28
    • 2012-09-10
    • 1970-01-01
    相关资源
    最近更新 更多