Modern C++ approach for providing optional arguments

Viewed 4256

Let's take the following function declaration:

void print(SomeType const* i);

Here, the const* nature of the argument i suggest the intent, that the parameter is optional, since it may be nullptr. If this was not intended, the argument would instead just be a const&. Communicating optional-semantics were certainly not the original intent for designing pointers, but using them to do so happens to work just fine for a long time.

Now, since using raw pointers is generally discouraged in modern C++ (and should be avoided in favor of std::unique_ptr and std::shared_ptr to precisely indicate particular ownership-semantics), I wonder how to properly indicate function parameters' optional-semantics without passing by value, i. e. copying, as

void print(std::optional<SomeType> i);

would do.

After thinking about it for a while I came up with the idea of using:

void print(std::optional<SomeType const&> i);

This would in fact be most precise. But it turns out that std::optional cannot have reference types

Also, using

void print(std::optional<SomeType> const& i);

would in no way be optimal, since then we would require our SomeType to exists in an std::optional on the caller-side, again possibly (or rather likely) requiring a copy there.

Question: So what would be a nice modern approach for allowing optional arguments without copying? Is using a raw pointer here still a reasonable approach in modern C++?


¹: Ironically the depicted reason for why std::optional cannot have reference types (controversy about rebinding or forwarding on assignment) does not apply in the case of std::optionals of const references, since they cannot be assigned to.

5 Answers

Accepting a raw pointer is perfectly fine and is still done in plenty of "modern" codebases (which I'll note is a fast-moving target). Just put a comment on the function saying that it's allowed to be null and whether the function holds a copy of the pointer after the call (i.e. what are the lifetime requirements for the pointed-to value).

Does function overloading provide a clean solution here? E.g. To declare both the const ref and empty param list versions of the function?
This may depend on what the function body does in the no argument/null case - and how you can manage the two implementations to minimize code overlap.

Raw pointers are usually fine for this type of optional argument passing, actually one of the only times it is fine to use raw pointers overall. This is also the canonical recommended way.

That being said, boost::optional does allow you to use reference optional and const reference optionals. It was decided against to have this feature in the std library (for reasons I leave out here).

Here, the const* nature of the argument i suggest the intent, that the parameter is optional since it may be nullptr.

[...]

So what would be a nice modern approach for allowing optional arguments without copying?

Allowing an optional argument (not in the std::optional sense, but in the semantic sense) with differing implementation variations based on whether the optional argument is present or not sound like an ideal candidate for overloading:

struct SomeType { int value; };

namespace detail {
    void my_print_impl(const SomeType& i) {
        std::cout << i.value;
    }    
}  // namespace detail

void my_print() {
    const SomeType default_i{42};
    detail::my_print_impl(default_i);
}

void my_print(const SomeType& i) { 
    detail::my_print_impl(i);
}

or

namespace detail {
    void my_print_impl() {
        std::cout << "always print me\n";
    }    
}  // namespace detail

void my_print() {
    detail::my_print_impl();
}

void my_print(const SomeType& i) {
    detail::my_print_impl();
    std::cout << "have some type: " << i.value;
}

or some similar variation, depending on what your implementation should do depending on the existence/non-existence of the optional argument.


Optional references, otherwise, are basically raw pointers, and the latter may just as well be used (if overloading is not applicable).

Related