1

Motivation

With C++ having gotten optional's (in C++17), it is now common to want to write the equivalent of:

If a condition holds, initialize my variable with some expression; and if the condition doesn't hold, keep my variable empty/unassigned

How would we write that in C++? Suppose our expression is of type int. The "ideal", or most convenient, syntax one might hope would work is:

auto myopt = some_condition ? std::nullopt : 123;

but optional is a library type, which the language knows nothing about, so it doesn't have much of a reason to harmonize nullopt_t with int into an optional<int>.

So, let's try being a little more verbose:

std::optional<int> myopt = some_condition ? std::nullopt : 123;

This still fails, as int can't be converted into nullopt_t and vice-versa.

We could write this:

auto myopt = some_condition ? std::nullopt : std::optional<int>(123);

which is ok-ish. But we're again in trouble if our type is not a plain integer but something more complex:

struct mystruct { int x; double y; };

/// -snip-

auto myopt = some_condition ? std::nullopt : std::optional<mystruct>{ 12, 3.4 };

... which fails to compile.

so,

Actual question

What is a decent, succinct (DRY...), single-instruction idiom for initializing an optional with either a value or nullopt? Note it must work for more complex types as well.

Note: C++ constructs introduced later than C++17 are also acceptable, but please mention which language version they're from.

5
  • It's probably worth mentioning in the question that while std::optional<mystruct>{ 12, 3.4 } fails to compile, std::optional<mystruct>{{ 12, 3.4 }} doesn't. Commented Jun 4 at 11:42
  • @JanSchultke: Actually, that could be its own answer. Want to write it?
    – einpoklum
    Commented Jun 4 at 11:45
  • I guess CTAD works too: std::optional{myclass{}}. C++20 for sure, but I guess C++17 might also work. The plus side is for copy/move the explicit typname can be removed: std::optional{"yes"s} must result in a std::optional<std::string>. However for none-aggregate types this invokes a move construction.
    – Red.Wave
    Commented Jun 4 at 12:30
  • @Red.Wave: I have a CTAD-based answer, please have a look at it.
    – einpoklum
    Commented Jun 4 at 12:44
  • @einpoklum saw it later, along with the comments.
    – Red.Wave
    Commented Jun 4 at 13:05

8 Answers 8

4

I would just do this:

std::optional<int> x;
if (condition)
    x = 42;

And for structs:

struct mystruct { int x; double y; };

std::optional<mystruct> x;

if (condition)
{
    // Pick one of:
    x = {.x = 1, .y = 2.3}; // (1)
    x = {1, 2.3}; // (2)
    x.emplace(1, 2.3); // (3)
}

Where (1) and (2) cost an extra move, and (3) is optimal.

(1) requires C++20. (3) requires C++20 if applied to aggregates (in C++17 you can add a constructor to the struct (with a parameter for each field) to make it work).

3
  • This is someone verbose in terms of number-of-statements, unless you wrap it in an IILE.
    – einpoklum
    Commented Jun 4 at 11:59
  • @einpoklum Not everything needs to be a oneliner. :) Why is an immediately invoked lambda better? (other than when you want to initialize a constant) Commented Jun 4 at 12:11
  • In general - use in contexts in which you must use a single expression.
    – einpoklum
    Commented Jun 4 at 12:46
3

Use std::make_optional

You can use the std::make_optional function:

auto myopt = some_condition ? std::nullopt : std::make_optional(123);

For mystruct, std::make_optional also works:

auto myopt = some_condition ? 
    std::nullopt : std::make_optional<mystruct>({ 12, 3.4 });

The braces can be eliminated, utilizing C++20 parenthesis initialization for aggregate types:

auto myopt = some_condition ? 
    std::nullopt : std::make_optional<mystruct>(12, 3.4);
2
  • What is the benefit of std::make_optional here, compared to just using a std::optional constructor and possibly relying on CTAD (class template argument deduction)? Commented Jun 4 at 11:33
  • 3
    @JanSchultke (1) Looks nicer (opinionated). (2) Avoids the explicit use of in_place. (3) std::optional(a) might indicate a copy if a is itself an optional, while std::make_optional(a) always wraps.
    – cpplearner
    Commented Jun 4 at 11:41
2

Write a utility function for it

If you want your initialization to as terse as possible, while still reasonably readable, you could write:

template <typename T>
auto value_if(T&& t, bool cond) {
    return cond ? std::nullopt : 
        std::optional<T>{std::in_place, std::forward<T>(t)};
}

and then use it like so:

auto myopt = value_if(mystruct{12, 3.4}, some_condition);

this is my favorite approach for now; but it does have the burden of having to remember yet-another-function.


Detriments of this approach:

  • Thanks @JanSchultke for drawing my attention to the case of T itself being an optional...
  • This forces the construction of a T value, while the question in the code does not.
  • This requires T to be movable. If it isn't, one can also write a utility function which constructs the T in place, but it would be more verbose.
4
  • 2
    Alternative 3.5: forward arguments into construction. auto value_if(bool cond, Args&& ...args)
    – Caleth
    Commented Jun 4 at 10:51
  • @Caleth: But that changes the order of arguments... hmmm. Also, you're not telling the compiled anything about mystruct. Perhaps you could post this as a separate answer?
    – einpoklum
    Commented Jun 4 at 10:54
  • 1
    This has the disadvantage that unlike the question code, the value needs to be computed, even if not used. We could instead pass an invocable that can be called to generate a T value, but that's getting unwieldy again... Commented Jun 4 at 11:10
  • @TobySpeight: Added this valid point to the answer.
    – einpoklum
    Commented Jun 4 at 11:24
1

Use deduction guides

Let's try a minimum-change-from-your-code approach.

Fortunately, in C++17 we didn't just get optional's, we also got class template argument deduction (CTAD) guides, specifically for optional's. So, this works:

auto myopt = some_condition ?
    std::nullopt : std::optional{ mystruct{ 12, 3.4 } };

and you've just replaced optional<T>{ ... } { with optional{ T{ ... } }. Not so bad.

3
  • This costs an extra move, I believe. Commented Jun 4 at 11:07
  • @HolyBlackCat: Well, one move for sure. Could it be two?
    – einpoklum
    Commented Jun 4 at 11:33
  • I believe it's always 1, but I'm not 100% sure. Commented Jun 4 at 11:50
1

Use an Immediately-invoked lambda expression

A slight refinement of @HolyBlackCat's answer:

auto myopt = [&]() {
    std::optional<mystruct> x;
    if (condition) { x.emplace(1, 2.3) }
    return x;
}();

Benefits:

  • Single statement as required.
  • No construction of mystruct's if the condition doesn't hold.
  • Only one construction of mystruct if the condition does hold, due to return value ellision.
  • No need to know anything about the standard library other than std::optional<T>.

Detriments:

  • Kind of verbose :-(
2
  • I feel like this is overly verbose and complicated when the alternative is auto myopt = condition ? std::nullopt : std::optional<mystruct>(std::in_place, 1, 2.3);. Commented Jun 4 at 11:46
  • I have that answer as well... but that one requires you to remember about another standard library facility, in_place_t; while this requires only knowledge about optional.
    – einpoklum
    Commented Jun 4 at 11:48
0

Use explicit emplacement-construction of std::optional.

(This is my least-favorite alternative, but one that utilizes a mechanism C++ provided straight-up.)

In your question, you're trying to initialzie the optional<T> with either a nullopt_t or a T [pr-value][1]. But - what about in-place construction of a T within the optional? Indeed, that is possible, though not very pretty, by explicitly saying you want to do so. optional<T> [has][2] the constructor:

template< class... Args >
constexpr explicit optional( std::in_place_t, Args&&... args );

and in your case, its use would mean writing:

auto myopt = some_condition ? 
    std::nullopt : std::optional<mystruct>{ std::in_place_t{}, 12, 3.4 };

which compiles and does what you want. DRY is observed, sort of - but it isn't very pretty.

0

Yet another option is to use an IILE (immediately invoked lambda expression):

auto myopt = [] -> std::optional<mystruct> {
    if (some_condition) return { 12, 3.4 };
    else                return std::nullopt;
}();

This works because the expressions within each return statement don't need to have a common type like in the conditional operator; they just need to be convertible to the explicitly specified return type.

Note that the first return statement calls constructor (8) and costs an unnecessary move. To avoid this, you'd write:

return std::optional<mystruct>(std::in_place, 12, 3.4);

To be fair, since you need to explicitly specify the type in this form (because the constructor is explicit), you could just as well use the conditional operator here:

auto myopt = condition ? std::nullopt : std::optional<mystruct>(std::in_place, 12, 3.4);

If the move of mystruct is inexpensive, you can also simply write:

auto myopt = condition ? std::nullopt : std::optional<mystruct>{{12, 3.4}};

All these solutions have the benefit that you can make myopt const if you want to.

2
  • So why not just correct the IILE to perform in-place construction and explain why you did so?
    – einpoklum
    Commented Jun 4 at 11:37
  • @einpoklum because it somewhat defeats the purpose of using an IILE and unnecessary calls to a move constructor are very rarely an issue, especially if the definition of the move constructor is visible. In practice, it's most likely going to be optimized out and I wouldn't worry about it outside of generic code, or types for which I know that moving is expensive. Commented Jun 4 at 11:40
0

From C++23, use monadic operations

Assuming some_condition depends on other std::optional state, you can use transform(), and_then() and or_else() to produce an optional from a different optional. It cannot be generalised to any condition (without shoehorning solutions into wrong tools), but from my experience in codebase that uses std::optional extensively you often find that you want to change one optional into another if it has value.

transform()

transform takes a function that is invoked if std::optional contains a value and returns a value to initialise another std::optional. The function should return object to be contained in std::optional, i.e., it should not return std::optional or std::nullopt.
Example:

std::optional<int> a {5};
auto b = a.transform([](int i) { return std::to_string(i); }); //b contains "5"s

std::optional<int> c {};
auto d = c.transform([](int i) { return std::to_string(i); }); //d contains nullopt

and_then()

and_then takes a function that is invoked with stored value if std::optional contains a value and returns an optional. This function should return an optional (not necessarily the same type as this).
Example:

auto filterValuesLessThanFiveAndConvertToDouble = [](int i) {
    return i < 5 ? std::make_optional<double>(i) : std::nullopt;
};
std::optional<int> a {5};
auto b = a.and_then(filterValuesLessThanFiveAndConvertToDouble); //b contains 5.0

std::optional<int> c {4};
auto d = c.and_then(filterValuesLessThanFiveAndConvertToDouble); //d contains nullopt

std::optional<int> e {};
auto f = e.and_then(filterValuesLessThanFiveAndConvertToDouble); //f contains nullopt

or_else()

or_else takes a function that is invoked if std::optional doesn't contain a value and returns an optional. Like and_then(), this function should return optional (not necessarily the same type as this).
Example:

std::optional<int> a {5};
auto b = a.or_else([](){return std::make_optional<int>(0);}); // b contains 5

std::optional<int> c {};
auto d = c.or_else([](){return std::make_optional<int>(0);}); // d contains 0
2
  • Monadic operations are useful and important, but not for the scenario of this question.
    – einpoklum
    Commented Jun 4 at 12:52
  • @einpoklum As I said, it's not a general answer, but an answer to a common version of some_condition that one may write. Commented Jun 4 at 12:54

Not the answer you're looking for? Browse other questions tagged or ask your own question.