Subtle C++ Compiler Error with std::optional and the Conditional Operator

An example of writing clear code with good intention, but getting an unexpected C++ compiler error.

Someone asked me for help with their C++ code. The code was something like this:

std::wstring something;
std::optional<bool> result = something.empty() ?
        ReadBoolValueFromRegistry() : {};

The programmer who wrote this code wanted to check whether the ‘something’ string was empty, and if it was, a boolean value had to be read from the Windows registry, and then stored into the std::optional result variable.

On the other hand, if the ‘something’ string was not empty, the std::optional result should be default-initialized to an empty optional (i.e. an optional that doesn’t contain any value).

That was the programmer’s intention. Unfortunately, their code failed to compile with Visual Studio 2019 (C++17 mode was enabled).

Partial screenshot of the C++ code that doesn't compile, showing squiggles under the opening brace.
The offending C++ code with squiggles under the {

There were squiggles under the opening brace {, and the Visual C++ compiler emitted the following error messages:

Error CodeDescription
C2059syntax error: ‘{‘
C2143syntax error: missing ‘;’ before ‘{‘

I was asked: “What’s the problem here? Are there limitations of using the {} syntax to specify nothing?”

This is a good question. So, clearly, the C++ compiler didn’t interpret the {} syntax as a way to default-initialize the std::optional in case the string was not empty (i.e. the second “branch” in the conditional ternary operator).

A first step to help the C++ compiler figuring out the programmer’s intention could be to be more explicit. So, instead of using {}, you can try and use the std::nullopt constant, which represents an optional that doesn’t store any value.

// Attempt to fix the code: replace {} with std::nullopt
std::optional<bool> result = something.empty() ?
        ReadBoolValueFromRegistry() : std::nullopt;

Unfortunately, this code doesn’t compile either.

Why is that?

Well, to figure that out, let’s take a look at the C++ conditional operator (?:). Consider the conditional operator in its generic form:

// C++ conditional operator ?:
exp1 ? exp2 : exp3

In the above code snippet, “exp2” is represented by the ReadBoolValueFromRegistry call. This function returns a bool. So, in this case the return type of the conditional operator is bool.

// Attempt to fix the code: replace {} with std::nullopt
std::optional<bool> result = something.empty() ?
        ReadBoolValueFromRegistry() : std::nullopt;
//             ^^^--- exp2             ^^^--- exp3
//             Type: bool

On the other hand, if you look at “exp3”, you see std::nullopt, which is a constant of type std::nullopt_t, not a simple bool value!

So, you have this kind of type mismatch, and the C++ compiler complains. This time, the error message is:

Error C2446 ‘:’: no conversion from ‘const std::nullopt_t’ to ‘bool’

So, to fix that code, I suggested to “massage” it a little bit, like this:

// Rewrite the following code:
//
// std::optional<bool> result = something.empty() ? 
//        ReadBoolValueFromRegistry() : {};
//
// in a slightly different manner, like this:
//
std::optional<bool> result{};
if (something.empty()) {
    result = ReadBoolValueFromRegistry();
}

Basically, you start with a default-initialized std::optional, which doesn’t contain any value. And then you assign the bool value read from the registry only if the particular condition is met.

The above C++ code compiles successfully, and does what was initially in the mind of the programmer.

P.S. I’m not a C++ “language lawyer”, but it would be great if the C++ language could be extended to allow the original simple code to just work:

std::optional<bool> result = something.empty() ?
        ReadBoolValueFromRegistry() : {};

5 thoughts on “Subtle C++ Compiler Error with std::optional and the Conditional Operator”

  1. `a?b:c` usually returns a “common type” between the types of b and c. So what you can do, is force both b and c to have the final type you want.
    For your use case:
    `const auto result = something.empty() ?
    std::optional(ReadBoolValueFromRegistry()) : std::optional();`
    Advantages:
    – Since you’re unambiguously defined the full type in the expression, you can use `auto` for the variable.
    – And by using only one expression to initialize the variable, you can make it `const` and not worry that someone could change it later.

    Like

    1. Gerald: Thanks for your comments. As typical of C++, we can achieve a goal in several different ways 🙂
      In any case, it would be great if the C++ standard could be improved, by allowing the original initial code, which to me seems very clear in the intention, and kind of “less noisy” than some fixes.

      Like

      1. Sure, it seems clear to us humans, and it would be very nice, but I don’t see it happening, it would be quite difficult to express in the language specifications:
        You’d want something like `T r = a ? b : c` to be interpreted as `T r = a ? T(b) : T(c)`, where the the ternary operator’s inner arguments would be somehow guided by the type T on the left of the assignment that’s outside of the ternary expression!
        And by extension you’d probably want `void f(T); … f(a ? b : c);` to also know that b and/or c should be converted to T.
        Write a paper! 😉

        Like

      2. As already written in the blog post, I’m not a C++ “language lawyer”. But my suggestion is that, at least in a specific case like this, during an initialization (and maybe an assignment via =), a rule could be added to the language stating that in such cases the {} syntax in a conditional operator expression should be interpreted as the default initialization for the object (in this case the object is an instance of std::optional).
        This is a suggestion I give to the C++ language lawyers who are in a position to write a paper 🙂 (and attend various ISO meetings, etc.)

        Like

  2. Correction: I forgot “ twice above.

    And further improvement:
    `const auto result = something.empty() ?
    std::optional(ReadBoolValueFromRegistry()) : std::nullopt;`
    The first alternative is fully defined, so the second can just be something that can be converted to the first one, we don’t need to repeat the full type.

    Liked by 1 person

Leave a comment