2

In a discussion with a colleague regarding the use of std::optional this came up. Say we have a function that takes 2 unique pointers and the 2nd argument is optional i.e. it can be null.

It can be implemented either via using std::optional or just setting the default value of 2nd param as nullptr i.e.

#include <string>
#include <functional>
#include <iostream>
#include <optional>
#include <memory>

class A {
  // body of class  
};

class B {
  // body of class  
};

void func_with_optional(std::unique_ptr<A> aptr, 
                        std::optional<std::unique_ptr<B>> bptr = std::nullopt) {
  std::cout << "func with std::optional called\n";
}
 
void func_without_optional(std::unique_ptr<A> aptr, 
                           std::unique_ptr<B> bptr = nullptr) {
  std::cout << "func without std::optional called\n";
}

unless the second argument is class& say B& or any other type where default can't be specified say we only have one argument and that's also optional or an optional return value. I see no difference in the above two functions. The default value serves sufficiently enough without the need of std::optional.

Am I missing something here? Does the std::optional one or the default value version has more leverage over another. May be, answer lies in lines of best coding practices and not necessarily in what compiler allows or not.

What is the more generally accepted version in the industry?

Augustin
  • 2,444
  • 23
  • 24
Hummingbird
  • 647
  • 8
  • 27
  • 6
    Is there a meaningful difference between passing `nullptr` and `std::nullopt` as the second argument? If not, then the version with `optional` simply has two different ways of representing the same value which does not seem useful to me. It could lead to confusion as other developers wonder if there is a difference, and what it might be. – François Andrieux Aug 27 '21 at 15:08
  • 3
    Why not just use overload? `void func_without_optional(std::unique_ptr aptr)` and `void func_without_optional(std::unique_ptr aptr,std::unique_ptr bptr)`? – Marek R Aug 27 '21 at 15:09
  • 1
    You could argue that using `std::optional` self-documents that the argument is expected to sometimes be omitted, but a `nullptr` default argument value offers the same self-documentation. – François Andrieux Aug 27 '21 at 15:11
  • 1
    I would not allow an `optional` past a code review, as it would provide two different ways to be "null" and responsible code would have to make both checks. `if(bptr&&*bptr)...` – Drew Dormann Aug 27 '21 at 15:17
  • @FrançoisAndrieux if you were you were to write such a function which way would you prefer 1) with ```std::nullopt``` or 2) ```nullptr``` ? or as per your 1st comment doesn't really matter as both are the same. but as @DrewDormann mentioned he could prefer 2nd. – Hummingbird Aug 27 '21 at 15:19
  • 4
    @Hummingbird I would write two overloads, one with a second argument and another without, which simply forwards the call to the first overload passing `nullptr` as the second argument. I would not use the `optional` version, and I personally prefer to avoid default argument values when possible. – François Andrieux Aug 27 '21 at 15:20
  • @Hummingbird: I don't understand the question. You don't see a reason to use a thing, so... don't use it. Nobody's forcing you to use it. As far as I know, nobody's *asking* you to use it either. So what is the concern? – Nicol Bolas Aug 27 '21 at 15:24
  • @FrançoisAndrieux why write 2 overloads and not use default argument? is it for better clarity of the function definition. if it's a long answer maybe you could point me in the direction from where I could read about this. – Hummingbird Aug 27 '21 at 15:26
  • @NicolBolas I want to know which of the above two choices is better code practice. Or maybe both are wrong and some other is better like others mentioned in the comments they would use neither and rather provide 2 overloads. – Hummingbird Aug 27 '21 at 15:29
  • 2
    @Hummingbird Default argument values have gotchas, such as using them with `virtual` member functions or using default argument values with side effects. For simplicity, I don't use them unless they are required or greatly improve readability. In this case, I don't think they help. A function that takes a `unique_ptr` by value is uncommon, one that takes two where one is optional is very unconventional. Enough so that, even if I used default argument values normally, I wouldn't here. I would use the overload as an opportunity to document the case where the argument is omitted. – François Andrieux Aug 27 '21 at 15:29
  • 1
    The other side effect with default arguments that I don't like is that they bake the default value into the callsite. So if you change the default argument in Foo.h, you'll need to recompile Bar.cpp to pick up the change. (A big deal if Foo is owned by Team Foo in San Jose, and Bar owned by Team Bar in Berlin.) Having two overloaded functions encapsulates the default value into the implementation rather than injecting it into the callsite. – Eljay Aug 27 '21 at 15:35
  • 1
    @Eljay: While that's true, when it comes to a defaulted `optional` that defaults to `nullopt`, that ought to be fine. It's the implementation of the function that will decide what to do with an empty optional, not the call-site. If you want to change the "default value", then you should logically want an empty optional to act like a "default value". So the change would be internal. – Nicol Bolas Aug 27 '21 at 15:39
  • 2
    @FrançoisAndrieux "A function that takes a unique_ptr by value is uncommon" - as unique pointers are non-copyable, isn't a `std::unique_ptr` non-reference parameter typically the preferred way to design an API where the semantics are "I'm taking over ownership"? – dfrib Aug 27 '21 at 16:11
  • @dfrib You could also use an rvalue reference instead. It is almost equivalent, but using an rvalue reference makes it possible for the function to have strong exception guaranties. But whichever method is used, sink functions are not all that common. I make a distinction between "uncommon" and "unusual" or "strange". In other words, I meant to say that sink functions that take a `std::unique_ptr` don't occur very often, but they aren't bad or unexpected. Having two such arguments would be very rare, in my experience. More so if exactly one of them is optional. – François Andrieux Aug 27 '21 at 19:23

3 Answers3

2

You may start with noting that

  • a std::optional may hold a value, or may be empty, and that
  • a std::unique_ptr may hold ownership of an object, or may be detached (nullptr).

A std::optional<std::unique_ptr>> object would need to consider both of these, in this context without a clear purpose. A detached std::unique_ptr may just as well represent the absence of an object as compared to an optional wrapping a pointer type that may not refer to an object. Back-ported versions of std::optional to, say, a legacy C++03 (pre-move-semantics) code base would typically just implements the underlying value as a pointer that may or may not point to an object. This argument would thus favor using the func_without_optional version.

However, and this may be a wave breaker, many devs like to avoid default arguments for various reasons (concerns with pollution of the default argument into call sites, confusing API's where default arguments are used in virtual functions), and instead consider simply overloading:

void func(std::unique_ptr<A> aptr, std::unique_ptr<B> bptr) {
  // Common impl.
}

void func(std::unique_ptr<A> aptr) { func(std::move(aptr), nullptr); }
dfrib
  • 70,367
  • 12
  • 127
  • 192
0

Differences:

  1. For std::optional you need C++17. With the default parameter, you could be using C++98.

  2. Lots of extra lines to compile (if you weren't including memory, string, etc and just using #include <optional> - it pulls nearly 7000 lines of code with gcc).

It is true, as @FrancoisAndrieux points out, that pointers have an inherent null value of their own, so wrapping them in an std::optional is not super-useful.

On the other hand - in a language with an optional type constructor, you don't really need null pointers. Suppose no pointer is null, i.e. all addresses are usable. This actually happens on some hardware. Then, functions can simply avoid pointer checks; and you would use a nullopt/None value to signify "no pointer" or "not pointing anywhere". See:

With std::optional standardizing, can we stop using nullptr in new code and deprecate it?

(but that's not the way things work in C++ officially, as commenters remind us.)

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • "*Suppose no pointer is null, i.e. all addresses are usable.*" And yet, `optional t{std::in_place};` contains a null pointer. If value initialization of the internal object will create a value your code considers an invalid value, maybe it's not a construct you should be using. – Nicol Bolas Aug 27 '21 at 15:26
  • `0` is required to be a null pointer constant: https://timsong-cpp.github.io/cppwp/conv.ptr#1 – NathanOliver Aug 27 '21 at 15:35
  • @NathanOliver: I was being hypothetical, "in a language". – einpoklum Aug 27 '21 at 16:05
  • @NicolBolas: I was being hypothetical, see edit. – einpoklum Aug 27 '21 at 16:06
  • In a previous job, we had convention that each smart pointer should not be null (we don't have smart reference :-/ and `gsl::not_null` doesn't work with `std::unique_ptr` to express that :-/). (The rare cases where `nullptr` was possible were documented and often introduce bug :-/ ). – Jarod42 Aug 27 '21 at 22:02
0

I prefer third solution: an overload with a single argument:

void func_without_defaults(std::unique_ptr<A> aptr, 
                           std::unique_ptr<B> bptr) {
  std::cout << "func without any default value\n";
}

void func_without_defaults(std::unique_ptr<A> aptr) {
  std::cout << "func with single argument\n";
  func_without_defaults(std::move(aptr), makeDefaultValueForB());
}

Quite often I see code with if checking for a default value.

Marek R
  • 32,568
  • 6
  • 55
  • 140