views:

158

answers:

5

My String class provides an operator char* overload to allow you to pass the string to C functions.

Unfortunately a colleague of mine just inadvertently discovered a bug.

He effectively had the following code.

StringT str;
// Some code.
delete str;

Is there anyway to prevent delete from casting the string object to a char* to prevent future bugs like this cropping up? std::string gets round this problem by not providing a char operator overload but, ideally, I'd like to keep the overload but prevent that delete from working.

+6  A: 

Is there anyway to prevent delete from casting the string object to a char* ...?

Yep: Shy away from implicit casting operators. That's been preached for at least a decade now. Believe in it and you will lead a happier life.

sbi
Well yeah I got that ;) I was just hoping there was some way to prevent the compiler automagically doing it in certain cases. I'm guessing probably not .. but always worth an ask ...
Goz
_Especially_ implicit conversions to pointers to PODs.
Timo Geusch
I think C++1x will allow you to stick an `explicit` in front of such operators in order to prevent implicit conversions. But, frankly, I don't see the advantage of `static_cast<const char*>(str)` over `str._c_str()`.
sbi
+4  A: 

Believe it or not, there is a reason std::string doesn't provide an implicit conversion, the c_str() function wasn't created just to irritate you. Provide implicit conversions and you open yourself up to a world of ambiguity and pain.

anon
A: 

Controversial opinion time: If someone writes code that suffers from this "bug," they deserve to get bitten.

To paraphrase your question:

How do I keep people from using my firearms to shoot themselves in the foot?

You can't. I'll disagree with @sbi's opinion and say that your overload is fine. If this causes problems in someone's code, it's because someone doesn't know C++ and shouldn't be coding it.

You have bigger problems to worry about than whether or not someone who doesn't understand the language well enough to know not to delete things that aren't pointers can abuse your class.

Caveat: I am relatively new to C++, and have not seen the horrors of some more grizzled veterans. It is possible that a sample of particularly bad ambiguity might convince me of my evil ways. This, however, is not it.

Chris Lutz
I beg to differ. While you cannot prevent people using firearms from shooting themselves in the foot, you can minimize the number of accidents. Any army has regulations for the use of firearms to do exactly this. See the rule to not to use implicit conversions as such a regulation. Stick to it and you will eliminate a few casualties.
sbi
Particularly bad example: `struct Foo { char *x; char getX() { return *x; } char operator*() { return *0; } /* other functions */ };`. Documentation says "Foo is a smart pointer, but don't ever dereference it, use `getX` instead". This is a "valid" class, it can be used correctly, but it's a poor interface. It's perfectly reasonable to want to improve the interface, by getting rid of the "trap" laid by having an `operator*` which compiles but doesn't run. Conversion operators are difficult precisely because they create such traps via implicit casting.
Steve Jessop
+6  A: 

Yes. Provide TWO implicit casts, by declaring (but not defining!) operator char const volatile*. When you're passing your StringT to a C string function, overload resolution will still select your original operator char const* (exact match). But delete str; now becomes ambiguous.

The declaration can be private, so if it would somehow be selected will be a compile-time error. The intended ambiguity occurs before overload resolution - the private only serves to catch the exceedingly rare cases where the volatile overload would be selected somehow.

MSalters
+1 works just as well without the `const`, if necessary.
Chris Lutz
Thats the kinda answer i was after :) Cheers!
Goz
This is indeed very clever. (+1) But it might open a whole other can of worms instead of closing the other one...
sbi
There are only 2 possible extra outcomes, really: either a situation becomes ambiguous (like the `delete` case), or the `volatile` overload is now selected. The latter can only happen in a conversion sequence that ends in a `volatile` pointer. That's exceedingly rare, and it would again be an error.
MSalters
@MSalters: I'd fully agree, except that passing class objects to `delete` should be exceedingly rare, too. Also, while these unwanted conversions now won't happen with `delete` anymore, they might come up in other contexts, where the overload resolution isn't ambiguous, but isn't wanted either. There's many good reasons that implicit conversions are discouraged.
sbi
Sorry, but I expanded the text to explicitly show why it is a bougs argument. But I cannot fully prove the _absence_ of such situations; you can give one concrete counterexample however.
MSalters
Wait, did you just ask for an example for implicit casts doing something unintended?
sbi
Not in general, but as a result of adding this specific conversion. So please show _sane_ code that breaks by adding the proposed `volatile` operator.
MSalters
@MSalters: I don't need to, since I regretted that your trick didn't eliminate `operator const char*`. That alone leaves lots of room for unwanted conversion. As for `operator const volatile char*` - I didn't claim I knew sane code which would break it, I only pointed out that, with the same certainty, probably everybody assumed `delete obj` to be insane code that wouldn't exist anywhere.
sbi
A: 
struct Dummy {};
class StringT {
public:
    // ........
    operator char*() { /*...*/ }
    operator const char*() const { /*...*/ }

    operator const Dummy* () const { return 0; }
    operator Dummy* () { return 0; }
};

/// ...
void g(const char * ) { /*...*/ }

int main() {

    StringT str;
    g(str); // OK
    delete str; //error C2440: 'delete' : cannot convert from 'StringT' to 'void *'
    return 0;
}
Alexey Malistov