Шаблон интеллектуального указателя C ++, который автоматически преобразуется в пустой указатель, но не может быть явно удален - PullRequest
5 голосов
/ 22 июля 2010

Я работаю в очень большой устаревшей кодовой базе C ++, которая останется безымянной. Будучи унаследованной кодовой базой, он передает необработанные указатели повсюду. Но мы постепенно пытаемся его модернизировать, поэтому есть и несколько шаблонов умных указателей. Эти интеллектуальные указатели (в отличие, скажем, от scoped_ptr Boost) имеют неявное преобразование в необработанный указатель, так что вы можете передать один из них в подпрограмму, которая принимает необработанный указатель без необходимости писать .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
}

Ответы [ 5 ]

7 голосов
/ 22 июля 2010

Стандарт гласит:

Операнд должен иметь тип указателя или тип класса, имеющий одну функцию преобразования (12.3.2) в тип указателя.Если операнд имеет тип класса, операнд преобразуется в тип указателя путем вызова вышеупомянутой функции преобразования, и преобразованный операнд используется вместо исходного операнда в оставшейся части этого раздела.

Вы можете (ab) -использовать отсутствие разрешения перегрузки, объявив постоянную версию функции преобразования.На соответствующем компиляторе этого достаточно, чтобы он больше не работал с 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 /* ... */ }

Однако у этого недостатка есть то, что ваш smartpointer теперь может быть конвертирован в любой указатель типа.Хотя фактическое преобразование все еще проверяется на типизацию, но это не исключает предварительные преобразования, а скорее приводит к ошибке времени компиляции намного позже.К сожалению, SFINAE не представляется возможным с функциями преобразования в C ++ 03 :) Другой способ - вернуть частный указатель вложенного типа из другой функции

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

Обратите внимание, что эти обходные пути необходимы только для не соответствующих требованиям компиляторов.

4 голосов
/ 22 июля 2010

Нет способа остановить одного, а не другого. Везде, где он может быть неявно преобразован в указатель для вызова функции, он может быть неявно преобразован для выражения удаления.

Лучше всего удалить функцию преобразования. В вашей ситуации именно поэтому определенные пользователем операторы преобразования опасны и не должны использоваться часто.


Я не прав. : (

1 голос
/ 22 июля 2010

Вы можете использовать технику, представленную Boost , но меня беспокоит то, что вы разрешаете неявные преобразования из интеллектуального указателя в необработанный указатель, который обычно не одобряется. Кроме того, пользователи могут вызывать delete по указателю, полученному оператором ->, так что на самом деле вы ничего не можете сделать, чтобы не дать определенному идиоту обойти любой механизм, который вы придумали.

Вы действительно должны просто реализовать метод get() вместо предоставления operator T*(), чтобы по крайней мере вызовы delete smartptr не компилировались. Не идиоты должны быть в состоянии понять, что, вероятно, есть причина, по которой это не сработает.

Да, набирать LegacyFunc(smartptr.get()) труднее, чем LegacyFunc(smartptr), но первый предпочтительнее, поскольку он делает его явным и предотвращает неожиданные преобразования, такие как delete smartptr.

Что если у вас есть такие функции:

 void LegacyOwnPointer(SomeType* ptr);

где функция будет где-то хранить указатель? Это испортит умный указатель, потому что теперь он не знает, что кому-то еще принадлежит необработанный указатель.

В любом случае, у вас есть работа. Умные указатели похожи на необработанные указатели, но они не одинаковы, поэтому вы не можете просто найти и заменить все экземпляры T* и заменить его на my_scoped_ptr<T> и ожидать, что он будет работать так же, как и раньше.

0 голосов
/ 23 июля 2010

Я вижу, где вы не хотите делать массовое применение .get ().Вы когда-нибудь задумывались о гораздо меньшей замене delete?

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); 
    } 
0 голосов
/ 23 июля 2010

не особо задумывался над этим, но ... Можете ли вы предоставить перегрузку для оператора delete, которая строго типизирована для экземпляров вашего шаблона, так что при включении кода компиляция завершится неудачей? если это в вашем заголовочном файле, то неявное преобразование в вызовах для удаления должно быть предотвращено в пользу вызова вашей перегрузки.

оператор delete (my_scoped_ptr) { // ... здесь идет некомпилируемый код }

Извинения, если это окажется глупой идеей.

...