8

boost::optional<T> (1.51) provides a way of constructing objects that is very dangerous for my users and that I'd like to prevent. Let's say I have my own integer class and I want to pass an optional such integer and store it in some class:

class myint {
public:
    int m_a;
    myint (int r_a) : m_a(r_a) {
    }
};

struct myclass {
    boost::optional<myint> content;
    myclass (const boost::optional<myint>& arg) : content(arg) {
    }
};

and now, here's how users would use the class:

myclass(myint(13));            //correct use
myclass(boost::none);          //correct use
myclass(myint(0));             //correct use
myclass(0);                    //INCORRECT use, this easy typo
                               //equates boost::none which
                               //is not what the user meant

I'd like to understand what is going on here and prevent this behaviour.


Interestingly,

myclass(1);              //does not compile

boost::none is totally a valid value for my field, but having a boost::none sneak-up when the user is trying to type in a 0 is horrendously misleading and dangerous.

The intent might be a bit hidden since I'm not really rolling out a myint class and I don't really have a class myclass that serves little to no purpose. Anyways I need to send 10 or so optional ints to a function and deduping wouldn't work. (you can imagine I asked you for your age, your height and your wealth and that there's three special buttons to check if you don't want to answer a question)


I've posted an answer that seems to work below (built from Mooing's Duck & Ilonesmiz suggestion, but lighter). I'm happy to hear comments about it, though.

Null
  • 1,950
  • 9
  • 30
  • 33
Gurg Hackpof
  • 1,304
  • 1
  • 13
  • 26
  • 3
    Ouch, seriously? That's quite annoying :( . – us2012 Mar 06 '13 at 18:02
  • want you allow boost::none itself? – RiaD Mar 06 '13 at 18:03
  • RiaD: I'm not sure what you mean. Can you please reformulate? us2012: This is monstruously dangerous for my application If that helps i have the feeling that it's coming from the copy constructor of myint somehow. – Gurg Hackpof Mar 06 '13 at 18:10
  • If you would disallow boost::none, you may make constructor `myclass (const myint& arg)` – RiaD Mar 06 '13 at 18:13
  • 1
    @GurgHackpof: if it happens with `0`, but not with `1`, that means it thinks `0` is the NULL pointer, which means it's being constructed with a _pointer_ constructor. A quick check confirms that `none_t` is a pointer type, so yeah, it's using `boost::none_t`. – Mooing Duck Mar 06 '13 at 18:14
  • 1
    @Gurg Yeah, I understand your problem. I was saying that I'm unhappy with the boost designers for making this possible (Although I have to admit I don't have a better implementation myself.) – us2012 Mar 06 '13 at 18:16
  • You are missing Mooing Duck's original int overload in your final solution. With that addition, the code you commented no longer results in a compiler error. You can see it [here](http://liveworkspace.org/code/34tpjJ$0). –  Mar 07 '13 at 13:36
  • 1
    @Ilonesmiz: I understand this, but blowing up at compilation is not a bad solution for me: dev are forced to adapt but at least they're not caught unaware – Gurg Hackpof Mar 07 '13 at 13:47

4 Answers4

3

Do not let the constructor take a boost::optional I would do something like this instead.

struct myclass {
    boost::optional<myint> content;
    myclass () = default;
    explicit myclass(const myint& int_):content(int_){}
};

However when I am thinking about it I am not completely clear on what you are trying to achieve and what you want to avoid happening. What is the purpose of the optional member?

Mooing Duck
  • 64,318
  • 19
  • 100
  • 158
AxelOmega
  • 974
  • 10
  • 18
  • Even with a `none_t` overload, [this still doesn't work](http://stacked-crooked.com/view?id=1f16afb966314f54c62cbe0fb892ce80-18aa934a8d82d638dde2147aa94cac94) :( – Mooing Duck Mar 06 '13 at 18:41
  • well I think the answer (after the edit) suffers from the same problem as the answer from FredericS: it does not scale to multiple parameters – Gurg Hackpof Mar 06 '13 at 18:42
  • @Mooning Duck thanks for the addendum, however my original idea was a class that is either in an initialized state or not. I still think a clarification of the intent from the OP is needed to fully understand the problem. – AxelOmega Mar 06 '13 at 18:43
  • @AxelOmega: I think I clarified, see the edit above. Don't hesitate to ask if I wasn't clear. – Gurg Hackpof Mar 06 '13 at 18:44
  • Closest thing to this answer that I can make work is [this](http://stacked-crooked.com/view?id=3982585ada10848cf10321611fdc275e-18aa934a8d82d638dde2147aa94cac94) – Mooing Duck Mar 06 '13 at 18:47
  • 1
    Why do you have to allow `boost::none`? Why not just allow `myclass()` or `myclass(17)`. I am still missing the intent, what are you trying to accomplish and why? Maybe there is a better way to achieve what you want. – AxelOmega Mar 06 '13 at 18:48
  • @AxelOmega: in the other answer, he hinted that the real `myclass` has _many_ members that get initialized, so he needs `myclass(0, none, 13);` – Mooing Duck Mar 06 '13 at 18:52
  • 1
    @MooingDuck,@AxelOmega : the intent might be a bit hidden since i'm not really rolling out an int class and I don't really have a class myclass that serves little to no purpose. Anyways I need to send 10 or so optional ints to a function and deduping the way you suggest wouldn't work. (you can imagine I asked you your age, your height and your wealth and that there's three special buttons to check if you don't want to answer a question) – Gurg Hackpof Mar 06 '13 at 18:53
  • 1
    Maybe `boost::parameter` can help you out. Only pass the parameters that you name. The rest default to `boost::none_t` (http://www.boost.org/doc/libs/1_53_0/libs/parameter/doc/html/index.html) – AxelOmega Mar 06 '13 at 18:59
  • @AxelOmega: I'll be looking into it, sounds like a plan – Gurg Hackpof Mar 06 '13 at 19:01
  • @AxelOmega: I looked it up, this really feels like an over-engineered solution so I dropped it. – Gurg Hackpof Mar 07 '13 at 08:45
3

This is uglier than I like, but it seems to address your concerns. It works by forwarding the argument given to myclass perfectly to a pair of functions that take either an int or a boost::none_t, bypassing the implicit user-defined constructor. This works because 0 matches int better than boost::none_t, and an implicit user-defined constructor is the worst match.

class myint {
public:
    int m_a;
    myint (int r_a) : m_a(r_a) {}
};    
boost::optional<myint> myintctor(int arg) {return myint(arg);}
boost::optional<myint> myintctor(boost::none_t arg) {return arg;}

struct myclass {
    boost::optional<myint> content0;
    boost::optional<myint> content1;
    boost::optional<myint> content2;

    template<class T0, class T1, class T2>
    myclass(const T0& a0, const T1& a1, const T2& a2) 
    :content0(myintctor(a0)), content1(myintctor(a1)), content2(myintctor(a2))
    {}
};

Proof of concept. Modern compilers ought to be smart enough to elide the copy, but that shouldn't matter for an int.

Mooing Duck
  • 64,318
  • 19
  • 100
  • 158
  • mmh, that is smart indeed! However This is still a bit annoying since I can totally see myself forgetting to pull out this trick for the next function I write. works though! – Gurg Hackpof Mar 06 '13 at 19:01
  • @GurgHackpof: Really, this should only occur for `boost::optional` where `T` has a _user-defined constructor_ that accepts `0`. 90% of the time you shouldn't need this trick. – Mooing Duck Mar 06 '13 at 19:03
  • you're missing a copy constructor (see edit above) but I'll go with that.Thanks! – Gurg Hackpof Mar 07 '13 at 08:43
  • Sorry man, I flagged that as an answer before noticing that it is not one. Problem is: Ì can't forward an existing `boost::optional` through this kind of structure. Look at my edit to the question and the attached example. – Gurg Hackpof Mar 07 '13 at 10:05
  • 1
    @GurgHackpof I haven't tested it thoroughly, but changing `boost::optional myintctor(boost::none_t arg) {return arg;}` with `boost::optional myintctor(boost::optional arg) {return arg;}` seems to work. –  Mar 07 '13 at 11:58
  • @Ilonesmiz: you're right! This salvages this answer (as per my edit in the answer above). I have no clue *why* this works though. – Gurg Hackpof Mar 07 '13 at 13:23
  • @Ilonesmizk: I think I found a lighter approach, below – Gurg Hackpof Mar 07 '13 at 13:51
1

I guess the problem is only meaningful for optional int. One solution could be to provide two constructors:

myclass() : content(boost::none) {}
myclass(myint input) : content(input) {}

It's true that you lose a bit the advantage of boost::optional...

FredericS
  • 413
  • 4
  • 9
  • It's really not a solution. if you've got to pass two of these guys you would need 4 constructors and so on. I have a function with 10 parameters or so so this would quickly become unmanageable. – Gurg Hackpof Mar 06 '13 at 18:27
  • @GurgHackpof: I don't think there's any other solution. `0` implicitly converts to `boost::none_t`, and there's nothing any of us can do about that. – Mooing Duck Mar 06 '13 at 18:44
  • @Mooing Duck: thanks for trying but I'm not all that happy with throwing the towel! (could roll out my own optional class, but that would be a pain) – Gurg Hackpof Mar 06 '13 at 18:47
1

This code (Inspired from Ilonesmiz) seems to do the job fine and is a bit lighter than the approach from Mooing Duck but still uses the magic templating trick.

struct myprotectedclass {
    boost::optional<myint> content;

    template <class T>
    myprotectedclass(const T& a) :content(boost::optional<myint>(a)) {}


};

Here is the proof.
When C++ sees the 0, it thinks "hmm, this is probably an int, but it might be a pointer to something!" (only for 0, no other numbers) But if you pass that 0 to a function, it must decide on a type, and so it picks the default of int. Whereas, in the original, a 0 was passed to a function expecting a myint or a pointer (boost::none_t is a pointer). 0 isn't a myint, but it can be a pointer, so it was picking that one.

Mooing Duck
  • 64,318
  • 19
  • 100
  • 158
Gurg Hackpof
  • 1,304
  • 1
  • 13
  • 26
  • 1
    I see your proof, but I don't understand it. From my understanding of the code, this shouldn't change anything. From my understanding of your test, nothing appears changed. With `myclass a(0);` it did not contain a value. With `myprotectedclass b(0)`, that _also_ did not contain a value. I fail to see any difference – Mooing Duck Mar 07 '13 at 17:52
  • I added more explanations to my answer to explain why and how it works. – Mooing Duck Mar 07 '13 at 17:55
  • @MooingDuck: Right, I messed up in the test, which pushed me into believing that folding in the template was working. Look at the code above (that I edited and I obviously fixed the test) and you'll understand why i did this mistake. The current one DOES work. – Gurg Hackpof Mar 07 '13 at 18:45
  • I can't imagine why this works when the original code didn't... oH! `const T& a` _forces_ `0` to be an `int` rather than a nullpointer! Heh, this doesn't work on _any_ of the same principles as mine, despite the fact it certainly looks like it does! :D – Mooing Duck Mar 07 '13 at 18:59
  • @MooingDuck: I must say I have a hard time with understanding it too. – Gurg Hackpof Mar 07 '13 at 19:04
  • @MooingDuck: would you mind expanding on the `const T&` forcing `0` to be an int ? – Gurg Hackpof Mar 07 '13 at 19:06
  • 2
    When C++ sees `0`, it thinks "hmm, this is probably an `int`, but it might be a pointer to something!" (only for `0`, no other numbers) But if you pass that `0` to a function, it must decide on a type, and so it picks the default of `int`. Whereas, in the original, you passed it to a function expecting a `myint` or a pointer (`boost::none_t` is a pointer). `0` isn't a `myint`, but it can be a pointer, so it picked that one. – Mooing Duck Mar 07 '13 at 19:09