Search code examples
c++templatesboostc++14

Function with single optional parameter and default value in template function


I want a function with has only 1 argument which is optional with generic type and has assigned boost::none as default value. Is that possible?

#include <iostream>
#include <string>
#include <boost/optional.hpp>

template <typename T>
void f(boost::optional<T> v = boost::none)
{
    if (v)
    {
        std::cout<<"v has value: " << v.get();
    }
    else
    {
        std::cout<<"v has no value!";
    }
}

int main()
{
   f(12);
   f("string");
   return 0;
}

Solution

  • Mmm the other answer is close. But not quite there. f(12) doesn't "try to instantiate f<int&&>". In fact, it fails to deduce T because T is in non-deduced context.

    Also, your question was beside the point: even without a default value you have the same problem: Compiler Explorer

    template <typename T> void f(boost::optional<T> v) {
        if (v) {
            std::cout<<"value: " << v.get() << "\n";
        } else {
            std::cout<<"no value\n";
        }
    }
    
    int main()
    {
       f(12);
       f("string");
    }
    

    Now, before I blindly show you how you can fix all that, ask yourself the question: What are we doing here.

    If you want default arguments, doesn't that by definition mean that they aren't optional values? Maybe you simply need: Compiler Explorer

    template <typename T> void f(T const& v) {
        std::cout << "value: " << v << "\n";
    }
    void f() {
        std::cout << "no value\n";
    }
    
    int main()
    {
       f(12);
       f("string");
       f();
    }
    

    Printing

    value: 12
    value: string
    no value
    

    With some hackery you can combine the overloads by defaulting the template type argument:

    template <typename T = struct not_given*> void f(T const& v = {}) {
        if constexpr(std::is_same_v<T, not_given*>) {
            std::cout << "no argument\n";
        } else {
            std::cout << "value: " << v << "\n";
        }
    }
    

    Prints Compiler Explorer

    value: 12
    value: string
    no argument
    

    What If You Require optional<>

    In that case, in you specific example you would probably want optional<T const&> to avoid needlessly copying all the arguments; but see std::optional specialization for reference types.

    If You Really Really Want¹

    Say, you MUST have the semantics you were looking for. You do not care that you won't be able to know the difference between calling with no argument vs. calling with an uninitialized optional (none). This is kinda like many scripting languages, right?

    Now you have to make the template argument become deduced context, and then want to ensure that... it is an optional<T>:

    template <typename T, typename = void> struct is_optional : std::false_type { };
    template <typename T> struct is_optional<boost::optional<T>> : std::true_type { };
    
    template <typename T = boost::optional<void*> >
    std::enable_if_t<is_optional<T>::value> f(T const& v = {}) {
        if (v) {
            std::cout << "value: " << *v << "\n";
        } else {
            std::cout << "no value\n";
        }
    }
    
    template <typename T>
    std::enable_if_t<not is_optional<T>::value> f(T const& v) {
        return f(boost::make_optional(v));
    }
    
    int main()
    {
       f(12);
       f("string");
       f();
    }
    

    One "advantage" is that that now you clearly see the copying being done.

    Another "advantage" is that now you can support std::optional the same way: https://godbolt.org/z/1Mhja83Wo

    template <typename T> struct is_optional<std::optional<T>> : std::true_type { };
    

    Summary

    I hope this answer gets the point across that C++ is not a dynamically typed language. This implies that the idea of optional arguments of "unknown" type is really not idiomatic. (It might be a bit unfortunate that Boost called it boost::none instead of e.g. std::nullopt, perhaps giving people associations with Python's None.)

    Instead, you can use static polymorphism. The simplest version of that was the first I showed, using function overloading.

    If you were to mimic a dynamic type interface in C++, you would probably use std::variant or std::any instead. To restrict the bound types you would use concepts (this is getting a bit deep, but see e.g. Boost Type Erasure).


    ¹ i really really really wanna zig a zig ah