0

This is a standard / good coding practice question.

I've recently started using std::optional in my codebase. I think it's great (and verbose) for specifying an argument that's optional, so long as a reference doesn't need to be passed to that function of course.

However, it doesn't really replace a default argument in a sense that I still need to specify a value when calling the function.

Is adding std::nullopt as a default value for an std::optional argument considered a good practice? Or is it redundant / overkill, as in a sense I could have just used a default argument without std::optional in that case?

As I mentioned, I like how std::optional makes the function definition verbose, so I was interested to know about other people's approach to this.

Adding a rather crude example below. The arguments in my codebase are (small) objects but here I'm just using integers:

#include <optional>

int Foo(const int number_a,
        const std::optional<int> number_b = std::nullopt,
        const std::optional<int> number_c = std::nullopt) {
  int result = number_a;
  if (number_b.has_value()) {
    result += number_b.value();
  }
  if (number_c.has_value()) {
    result += number_c.value();
  }
  return result;
}

Any thoughts and suggestions are appreciated, thank you.

heothesennoc
  • 541
  • 5
  • 10
  • This is kinda opinion-based, but fwiw, I don't like it. It adds yet more cognitive load for would-be users of the function. –  Jan 20 '22 at 19:16
  • I prefer `std::optional` exclusively for functions that may or may not have to return a value. – sweenish Jan 20 '22 at 19:21
  • 2
    If you want default arguments, I'd say just provide default arguments or overload sets. If you want people to have to explicitly spell out "I'm choosing not to pass a value here", take an `std::optional`. – Nathan Pierson Jan 20 '22 at 19:21

1 Answers1

0
  1. Yes, that is perfectly fine. It may be slightly verbose, but the interface is very clear. The only problem is that if someone wants to specify the last optional parameter, they will also have to specify everything before that. A hacky workaround could be:
#include <optional>

struct FooOptArgs {
  std::optional<int> opt_arg1 = std::nullopt;
  std::optional<int> opt_arg2 = std::nullopt;
};

int Foo(int arg, FooOptArgs args = {});

int main() {
  Foo(40);
  Foo(40, {.opt_arg1 = 41});
  Foo(40, {.opt_arg2 = 42});
  Foo(40, {.opt_arg1 = 41, .opt_arg2 = 42});
}

It is kind of neat.

  1. If you want to pass in an optional reference, that can be done with regular pointers:
void foo(int* optionalInt = nullptr);
Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • For solution #2, that could entail forgetting to check for null before dereferencing. A bug for later. Alternatively, using `std::optional>` would enforce unpacking the int by the compiler, which seems what the OP is looking for — compiler provided seatbelts. (I'm withholding judgement on the merits of the OP's approach.) – Eljay Jan 20 '22 at 20:10
  • @Eljay Well, derefencing `std::optional` without checking is equally undefined, so I don't see what benefit it would have. If your default argument is null, many IDEs will warn you about "possible dereferencing null", but not as many IDEs have embraced `std::optional` yet. – Aykhan Hagverdili Jan 20 '22 at 20:46
  • Hmm, apparently it is turtles all the way down. I suppose the only way to avoid the problem is to use a language that doesn't have that capability, like Logo! – Eljay Jan 20 '22 at 21:05
  • 1
    @Eljay optional has `value()` which throws if null. But if you are careful enough to use that and not the arrow operator or the dereference operator, you might as well do the null check. – Aykhan Hagverdili Jan 20 '22 at 21:11
  • Does C++17 support out of order braced-enclosed initialization for structs, as mentioned in the example `Foo(40, {.opt_arg2 = 42})`? I thought that it doesn't.. – heothesennoc Jan 20 '22 at 23:07
  • @heothesennoc it is a C++20 feature – Aykhan Hagverdili Jan 21 '22 at 05:41