17

For example, instead of

void shared_ptr::reset() noexcept;
template <typename Y>
void shared_ptr::reset(Y* ptr);

one may think of

template <typename Y = T>
void shared_ptr::reset(Y* ptr = nullptr);

I think performance difference is negligible here, and the second version is more concise. Is there any specific reason the C++ standard goes the first way?

The same question has been asked for the Kotlin language, and default argument is preferred there.

Update:

std::unique_ptr::reset() follows the default argument design (see here). So I think the reason std::shared_ptr::reset() uses overloads is because they have different exception specifications.

Lingxi
  • 14,579
  • 2
  • 37
  • 93
  • You know, I'm not so sure of my (deleted) answer anymore. The version you proposed should be okay (minus the weaker exception specification). – StoryTeller - Unslander Monica Feb 18 '18 at 08:59
  • 3
    Don't know about the specific motivation of the ISO committee, but generally speaking, having different functions (whether overloaded or even with different names) results in a more static structuring of the program flow. This can be an advantage in itself; for example, you and your IDE know exactly where each version is called, which makes both manual and automatic code analysis easier. You can also set debugging breakpoints specifically for one version or the other. – Christian Hackl Feb 18 '18 at 09:02
  • 3
    Oh, and I'd be weary of applying any Kotlin guidelines in other programming languages. Kotlin is a very young language (not even 8 years old), it had a tiny user base before Google picked it up for Android development, and I suppose even most users today know it only in the context of Android frontend development. Kotlin certainly does look interesting, but there can be no truly tried and tested guidelines for a new programming language, because nobody can have tried and tested them. Compare that with C++, which has been around for 33 years. – Christian Hackl Feb 18 '18 at 09:19
  • 1
    There *is* [something in the CppCoreGuidelines](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-default-args), though. – Justin Feb 18 '18 at 21:37

4 Answers4

29

The crucial difference, is that the two operations are in fact not semantically the same.

The first is meant the leave the shared_ptr without a managed object. The second is meant to have the pointer manage another object. That's an important distinction. Implementing it in a single function would mean that we'll essentially have one function do two different operations.

Furthermore, each operation may have different constraints on the types in question. If we dump them into one function, then "both branches" will have to satisfy the same constraints, and that's needlessly restrictive. C++17 and constexpr if mitigate it, but those functions were specified before that exited.

Ultimately, I think this design is in line with Scott Meyers' advice. If the default argument has you doing something semantically different, it should probably be another overload.


Okay, so to address your edit. Yes, the exception specifications are different. But like I alluded to previously, the reason they can be different, is that the functions are doing different things. The semantics of the reset members require this:

void reset() noexcept;

Effects: Equivalent to shared_­ptr().swap(*this).

template<class Y> void reset(Y* p);

Effects: Equivalent to shared_­ptr(p).swap(*this).

Not a big newsflash there. Each function has the effect of constructing a new shared_ptr with the given argument (or lack thereof), and swapping. So what do the shared_ptr constructors do? According to a preceding section, they do this:

constexpr shared_ptr() noexcept;

Effects: Constructs an empty shared_­ptr object.
Postconditions: use_­count() == 0 && get() == nullptr.

template<class Y> explicit shared_ptr(Y* p);

Postconditions: use_­count() == 1 && get() == p. Throws: bad_­alloc, or an implementation-defined exception when a resource other than memory could not be obtained

Note the different post conditions on the pointer's use count. That means that the second overload needs to account for any internal bookkeeping. And very likely allocate storage for it. The two overloaded constructors do different things, and like I previously said, that's a strong hint to separate them into different functions. The fact one can get a stronger exception guarantee is further testament to the soundness of that design choice.

And finally, why does unique_ptr have only one overload for both actions? Because the default value doesn't change the semantics. It just has to keep track of the new pointer value. The fact that value is null (either from the default argument or otherwise), doesn't change the function's behavior drastically. A single overload is therefore sound.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 13
    >> *If the default argument has you doing something semantically different, it should probably be another overload.* Nice. – Nawaz Feb 18 '18 at 09:35
  • Among all the answers, yours is the most appealing to me :) – Lingxi Feb 18 '18 at 11:44
  • 3
    I believe I've naturally followed this advice in the past, without ever really thinking about it. Actually reading it stated explicitly makes a lot of sense. I will be much more aware of this now - thanks! – Lightness Races in Orbit Feb 19 '18 at 01:31
  • I'm confused. What semantic difference is there between `shared_ptr::reset(static_cast(nullptr))` and `shared_ptr::reset()`? Is the first one not legal? – user541686 Feb 19 '18 at 01:58
  • @Mehrdad - It isn't illegal per se. But that machinery we have to employ is a hint that we better use the other overload. If we insist, then we'll end up creating a shared state to manage a null pointer. Nothing illegal, just wasteful. – StoryTeller - Unslander Monica Feb 19 '18 at 05:31
  • 1
    @StoryTeller: OH, you're saying it will still have the control block there even if the pointer is `nullptr`?! That's pretty mind-boggling. Thanks! – user541686 Feb 19 '18 at 05:39
  • I've updated the question. `std::unique_ptr::reset()` does use the default argument design. – Lingxi Mar 13 '18 at 12:29
  • @Lingxi - Updated accordingly. – StoryTeller - Unslander Monica Mar 13 '18 at 13:13
7

If you are OFTEN resetting to precisely nullptr rather than a new value, then the separate function void shared_ptr::reset() noexcept; will have a space advantage, since you can use the one function for all types Y, rather than have a specific function that takes a Y type for every type of Y. A further space advantage is that the implementation without an argument doesn't need an argument passed into the function.

Of course, neither matters much if the function is called many times.

There is also difference in the exception behaviour, which can be highly important, and I believe this is the motiviation as to why the standard has multiple declarations of this function.

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
6

While the design choices of the other answers are all valid, they do assume one thing that does not fully apply here: Semantic equivalence!

void shared_ptr::reset() noexcept;
                      // ^^^^^^^^
template <typename Y>
void shared_ptr::reset(Y* ptr);

The first overload is noexcept, while the second overload isn't. There is no way to decide the noexcept-ness based on the runtime value of the argument, so the different overloads are needed.

Some background information about the reason for the different noexcept specifications: reset() does not throw since it is assumed that the destructor of the previously contained object does not throw. But the second overload might additionally need to allocate a new control block for the shared pointer state, which will throw std::bad_alloc if the allocation fails. (And resetting to a nullptr can be done without allocating a control block.)

anderas
  • 5,744
  • 30
  • 49
3

There is a fundamental difference between an overload and a default pointer:

  • the overload is self contained: the code in the library is completely independent of the calling context.
  • the default parameter is not self contained but depend on the declaration used in the calling context. It can be redefined in a given scope with a simple declaration (e.g. a different default value, or no default value anymore.

So semantically speaking, the default value is a short-cut embeded in the calling code, whereas the overload is a meaning embedded in the called code.

Christophe
  • 68,716
  • 7
  • 72
  • 138