39

I have defined a struct, which has a constructor:

struct MyStruct
{
    MyStruct(const int value)
        : value(value)
    {
    }
    int value;
};

and the following objects:

int main()
{
    MyStruct a (true);
    MyStruct b {true};
}

But I haven't received any compile errors, either with MVS2015 or Xcode 7.3.1.

  1. Why am I not getting any compile errors?
  2. How do I make the compiler help me detect this? (Initially, the struct was written to have bool data, but after some time, code changed and bool became int and several bugs were introduced.)
muru
  • 4,723
  • 1
  • 34
  • 78
T M
  • 3,195
  • 2
  • 31
  • 52

5 Answers5

72

A bool can be implicitly converted to an int in a way that's value preserving. The only disallowed conversions with brace initialization are narrowing conversions (e.g. the reverse bool{42}).

If you want to ensure that your class is constructible only with int, then the direct way is simply to delete all the other constructors:

struct MyStruct
{
    explicit MyStruct(int i) : value(i) { }

    template <typename T>
    MyStruct(T t) = delete;

    int value;
};

Here, MyStruct{true} and MyStruct(false) will yield calls to MyStruct::MyStruct<bool>, which is defined as deleted and hence is ill-formed.

The advantage of this over static_assert is that all the type traits will actually yield the correct values. For instance, std::is_constructible<MyStruct, bool> is std::false_type.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 4
    Yeah, that's better. – Lightness Races in Orbit May 12 '16 at 15:58
  • 1
    Very nice, good point that `is_constructible` doesn't consider deleted overloads. – Kerrek SB May 12 '16 at 16:03
  • 2
    "narrowing" is a misnomer... it implies that one is narrower than the other, however `int->double` and `double->int` are both considered "narrowing". Maybe "lossy" would be a better description – M.M May 12 '16 at 21:26
  • 5
    @M.M It's what the standard [calls it](http://eel.is/c++draft/dcl.init.list#7) for better or for worse, so I'll use it for consistency. – Barry May 12 '16 at 21:43
15

Here's a construction that allows you to only initialize your class from an int value:

#include <type_traits>

struct MyStruct
{
    template <typename T>
    MyStruct(T t) : value(t)
    {
        static_assert(std::is_same<T, int>::value, "Bad!");
    }

    int value;
};

That's because the template argument deduction required by this constructor template will produce the exact type of the argument and not perform conversions, so you can perform tests on that type.

You should perhaps also or instead use SFINAE to constrain the constructor, so that MyStruct doesn't present itself as constructible from anything.

Furthermore, you should probably also make the constructor template explicit so that random integers don't become MyStruct instances.

In other words, I'd write it like so:

struct MyStruct
{
    template <typename T,
              typename = std::enable_if_t<std::is_same<T, int>::value>>
    MyStruct(T t) : value(t) {}

    // ...
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
8

The simplest solution is to declare a bool constructor as deleted isn't it?

struct MyStruct
{
    MyStruct(bool) = delete;

    MyStruct(const int value)
    : value(value)
    {
    }
    int value;
};

example error output:

...
/Users/xxxxxxx/play/fast_return/skeleton/main.cpp:68:14: error: call to deleted constructor of 'MyStruct'
    MyStruct b {true};
             ^ ~~~~~~
/Users/xxxxxxx/play/fast_return/skeleton/main.cpp:57:9: note: 'MyStruct' has been explicitly marked deleted here
        MyStruct(bool) = delete;
        ^
2 errors generated.
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 1
    Yeah but now you have to do that for all the other things that convert to `int`, too. – Lightness Races in Orbit May 12 '16 at 15:59
  • @LightnessRacesinOrbit true, but I'm struggling to see that as a problem. The compiler will catch narrowing conversions if the OP uses brace-initialisation as per his example. And if he needs specific constructors for each type of int then he's going to have to write (or template) specific constructors anyway. – Richard Hodges May 12 '16 at 16:01
  • 1
    Will brace-initialisation catch `short`->`int` conversion? That's already one more constructor you need to manually `delete`. And how do you know your users will use brace-initialisation? Didn't get your last sentence; OP wants _one_ working constructor. – Lightness Races in Orbit May 12 '16 at 16:03
  • 4
    @LightnessRacesinOrbit no, short->int isn't narrowing, but neither can I see it being (semantically) a problem to allow the conversion. It's a natural conversion, compatible with all other uses of short ints. Preventing instantiation from a bool (sort of) makes sense, because a bool is subtly different in meaning to an int in terms of human reasoning. – Richard Hodges May 12 '16 at 16:06
  • Okay well I don't think I disagree with you, but that's not what the OP asked for. – Lightness Races in Orbit May 12 '16 at 16:44
  • @LightnessRacesinOrbit (pendantry alert) I didn't answer his question 1 as I think it's covered by the other answers well enough. Question 2 merely asks for help in finding 'this' problem, which was a design decision to convert from bool to int as the data type. While I agree that the question is open to interpretation, I think with respect, that I have answered Q2, as far as I understand his predicament and meaning to be. I.e. he wants to catch all places where a bool is used at the call site. – Richard Hodges May 12 '16 at 16:54
  • 1
    Oh yeah you've certainly done that. I was only critiquing the solution as it compares to the others :) – Lightness Races in Orbit May 12 '16 at 16:56
  • 1
    My interpretation here but I believe this perfectly answers the OP question, specially considering "*Initially struct was written to have bool data*". – Margaret Bloom May 12 '16 at 19:25
7

Because a bool can be implicitly converted to an int.

This is a language feature that you cannot turn off, sorry.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • I tried and `explicit` doesn't prevent this. Don't we need `explicit` on the built-in int? :) – wally May 12 '16 at 15:50
  • 1
    @flatmouse: Haha damn – Lightness Races in Orbit May 12 '16 at 15:50
  • @LightnessRacesinOrbit So no way to catch this kind of staff during compile time? – T M May 12 '16 at 15:55
  • 1
    @TM: Kerrek already showed what is basically your only possibility. It's a little heavy-handed for me, fundamentally altering what your constructor is and how it works. But that may be fine for you. Otherwise, no, no way. – Lightness Races in Orbit May 12 '16 at 15:56
  • I have seen worse things, namely `false` being promoted to a `QString`. I guess because `false` is 0, which is `NULL`, which is a `const char *`, and there is a constructor `QString(const char *)`. – Giorgio May 12 '16 at 18:29
  • @Giorgio: `NULL` is not a `const char*` but I take your point – Lightness Races in Orbit May 12 '16 at 21:08
  • That was my explanation fot why the compiler did not complain about a function with return type `QString` returning `false`. Doesn't `NULL` convert to any pointer type? – Giorgio May 12 '16 at 21:24
  • @Giorgio: "converts to" is not the same as "is", is what I'm saying – Lightness Races in Orbit May 12 '16 at 21:32
  • @LightnessRacesinOrbit: As far as I know only one implicit conversion is performed (but I may be wrong). In this case, from `const char *` to `QString` via the constructor `QStrring(const char *)`.. So it must be that `false` is considered a `const char *`. I'll have to look up the exactrules that are applied, because I am not sure. – Giorgio May 13 '16 at 04:25
  • 1
    @Giorgio: It may just be a language issue but to say that "`false` is considered a `const char *`" is absolutely wrong. – Lightness Races in Orbit May 13 '16 at 09:26
  • @Giorgio `false` is definitely not considered a `const char*`. But `QString(false )` probably invokes `QString(QChar)` which invokes `QChar(int)`. – Barry May 13 '16 at 11:48
  • 1
    @Barry: Invoking QString(QChar(int)) would involve two conversions. As far as I know, at most one implicit conversion is allowed. – Giorgio May 13 '16 at 15:18
  • I have created a question to try and clarify what is happening in my example: http://stackoverflow.com/questions/37214420/conversion-of-false-to-object-via-const-char-constructor – Giorgio May 13 '16 at 15:55
  • 1
    @Giorgio Yeah, one standard conversion and one user-defined conversion. That's perfectly fine. You just can't have more than one of either. – Barry May 13 '16 at 17:19
0

the value true is converted to value 1 (int).