views:

266

answers:

5

I am working in a very large legacy C++ code base which shall remain nameless. Being a legacy code base, it passes raw pointers around all over the place. But we are gradually trying to modernize it and so there are some smart pointer templates as well. These smart pointers (unlike, say, Boost's scoped_ptr) have an implicit conversion to the raw pointer, so that you can pass one of them into a routine that takes a raw pointer without having to write .get(). A big downside of this is that you can also accidentally use one in a delete statement, and then you have a double free bug, which can be a real pain to track down.

Is there a way to modify the template so that it still has the implicit conversion to the raw pointer, but causes a compile error if used in a delete statement? Like this:

#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
}
+1  A: 

You can use a technique presented by Boost, but my concern is that you're allowing implicit conversions from a smart pointer to a raw pointer, which is generally frowned upon on. Besides, users can call delete on a pointer obtained by the -> operator, so there's really nothing you can do to prevent a determined idiot to work around whatever mechanism you come up with.

You really should just implement a get() method instead of providing operator T*() so that at least calls to delete smartptr will not compile. Non-idiots should be able to figure out that there's probably a reason why that won't work.

Yes, it's more work to type out LegacyFunc(smartptr.get()) than LegacyFunc(smartptr), but the former is preferred since it makes it explicit and prevents unexpected conversions from happening, like delete smartptr.

What if you have functions like this:

 void LegacyOwnPointer(SomeType* ptr);

where the function will store the pointer somewhere? This will screw up the smart pointer, because now it's not aware that something else is owning the raw pointer.

Either way, you have some work to do. Smart pointers are like raw pointers, but they are not the same, so you can't just find-and-replace all instances of T* and replace it with my_scoped_ptr<T> and expect it to work just as well as before.

In silico
That's a different problem.
GMan
Whether or not it is frowned upon, it would be hopeless to try to shoehorn smart pointers into this legacy code base without an implicit conversion to the raw pointer. There's just too many places where you need to provide the raw pointer. -- Your suggestion only helps if I can modify the pointed-to class, which I can't, in general. I need a technique that works entirely within the smart pointer class.
Zack
I really don't think you appreciate the realities of working in a 3-million-line, 15-year-old code base. If I took the implicit conversion out of the smart pointer template (which is already heavily used), I would have to put in *thousands* of .get() calls and my coworkers would lynch me.
Zack
@Zack: Sounds like a good days work. :)
GMan
+4  A: 

There isn't a way to stop one and not the other. Anywhere it can be implicitly converted to a pointer for a function call, it can be implicitly converted for a delete expression.

Your best bet is to remove the conversion function. Your situation is exactly why user-defined conversion operators are dangerous and shouldn't be used often.


I'm wrong. :(

GMan
See my response to "In silicio". I am aware of the arguments against implicit conversions, but it simply isn't practical to do without them in this context.
Zack
@Zack: Then you'll just have to not delete them. That's your choice: Remove implicit conversion and use explicit `get()` for functions, or leave implicit conversions and hope delete isn't called on it.
GMan
I fear you are right, but I'm going to leave the question open for a day or so in case someone comes up with something clever.
Zack
@Zack: removing implicit conversions may be tedious, but once it's done you'll know that they won't cause problems in the future. To me, that would be worth quite a bit of mindless drudgery.
Mike Seymour
@Zack: Of course it would be freaking litb. Sadly his solution doesn't work on VC++.
GMan
@GMan: I also immediately thought "make the delete conversion ambiguous", but litb covered that answer pretty thoroughly before I saw the question. BTW what part of that doesn't work in VC++?
Ben Voigt
@Ben: It allows it. It picks the non-const variant both times. It does this in CS2008 and VS2010; I'll report a bug. (If I can't find one already open on it, I'll search first.)
GMan
What about the set (`operator T*()`, `operator void*()`)? Also please post the bug link so we can upvote it.
Ben Voigt
@Ben: Accepts that too, preferring the `void*` one. (Take the `void*` one out, get an error.) It successfully becomes ambiguous when using the `nested` trick, and remains ambiguous with respect to `void*` with the `fty` one. I'll type it up in a few.
GMan
+6  A: 

The Standard says

The operand shall have a pointer type, or a class type having a single conversion function (12.3.2) to a pointer type. If the operand has a class type, the operand is converted to a pointer type by calling the above-mentioned conversion function, and the converted operand is used in place of the original operand for the remainder of this section.

You can (ab)-use the absence of overload resolution by declaring a const version of the conversion function. On a conforming compiler that's enough to make it not work anymore with 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
}

Results in the following

[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.

On compilers that are less conforming in that regard (EDG/Comeau, GCC) you can make the conversion function a template. delete does not expect a particular type, so this would work:

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

However, this has the downside that your smartpointer is now convertible to any pointer-type. Although the actual conversion is still typechecked, but this won't rule out conversions up-front but rather give a compile time error much later. Sadly, SFINAE does not seem to be possible with conversion functions in C++03 :) A different way is to return a private nested type pointer from the other function

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

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

The only problem now is with a conversion to void*, in which case both conversion functions are equally viable. A work-around suggested by @Luther is to return a function pointer type from the other conversion function, which works with both GCC and Comeau and gets rid of the void* problem while having no other problems on the usual conversion paths, unlike the template solution

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

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

Notice that these workarounds are only needed for compilers that are not conforming, though.

Johannes Schaub - litb
You would do that, wouldn't you.
GMan
g++ is undecided, 4.4 doesn't compile, 4.5 does. If I change the type of the second conversion to a function pointer type, then g++ gets it right and stops at 'delete a'.
Luther Blissett
@Luther thanks, added :)
Johannes Schaub - litb
I reported the bug: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=45033
Johannes Schaub - litb
Shouldn't it be `int* operator();` and `const int* operator() const;`?
Ben Voigt
@Ben conversion functions have the type after the `operator` keyword. Applying the `const` twice will fail if they try to convert a `smart_ptr<T*> const` to `T*`. So both conversion functions return `T*` (a const pointer, after all, is not the same as a pointer to const. `smart_ptr<T const*>` of course would convert to `T const*`, which is the same what raw pointers would do).
Johannes Schaub - litb
Yeah, my bad on the syntax. And I was thinking how const-ness of `this` is imputed to members, but it would still be a `T* const` and not a `const T*`, so the return type stays `T*` as you showed.
Ben Voigt
This is great, and *almost* works for my use case, but it throws up hairballs on things like `if (smart_ptr)`, `if (smart_ptr == 0)`, `if (smart_ptr != 0)`, which are all over the code and apparently were relying on the one-and-only implicit conversion to `T*` before. Now they all get ambiguous overload errors. Adding an `operator bool` makes some but not all of them go away; I'm reluctant to add an `operator int`, and I don't understand what boost's smart_ptr/detail/operator_bool.hpp is doing.
Zack
A: 

have not thought much about this but ... Can you provide an overload for operator delete which is strongly-typed for instances of your template class such that when the code is included compilation fails? if this is in your header file then implicit conversion in calls to delete should be prevented in favour of a call to your overload.

operator delete(my_scoped_ptr) { //... uncompilable code goes here }

Apologies if this turns out to be a stupid idea.

Steve Townsend
No, that would get invoked for expressions of "delete pointer-to-smart-pointer", but not for "delete smart-pointer" which is causing the problem.
Ben Voigt
A: 

I can see where you do not want to do a massive application of .get()'s. Have you ever consider a much smaller replacement of delete's?

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); 
    } 
jyoung
This only helps if I can modify the pointed-to class, which I can't in general.
Zack