Search code examples
c++templatesc++17

Returning an optional from a templated conversion function


I would like to update conversion functions which have the following signatures:

template<typename In, typename Out>
bool Convert(const In& p_in, Out& p_out);

// Returns `true` if the conversion succeeded, `false` otherwise.

so that I could use std::optional instead. Something like (notice template parameters were switched):

template<typename Out, typename In>
std::optional<Out> Convert(const In& p_in);

There is a big change as far as templates are concerned: One of the templated type is now used in the return type of Convert. Because of this, compilation fails unless I specify function parameters. Example:

#include <optional>

class A
{
public:
    A() = default;

    A(int p_value)
    : m_value(p_value)
    {}

private:
    int m_value = 0;
};

template<typename In, typename Out>
bool Convert(const In& p_in, Out& p_out)
{
    return false;
}

template<>
bool Convert(const int& p_in, A& p_out)
{
    p_out = A(p_in);
    return true;
}

template<typename Out, typename In>
std::optional<Out> Convert2(const In& p_in)
{
    return std::nullopt;
}

template<>
std::optional<A> Convert2(const int& p_in)
{
    return A(p_in);
}

int main()
{
    const int in = 2;

    // Fine, but not ideal since we have `std::optional` available:
    A out;
    const bool result1 = Convert(in, out);

    // Failed:
    //const std::optional<A> result2 = Convert2(in);

    // Ok, but sadly A is repeated at called site:
    const std::optional<A> result3 = Convert2<A>(in);

    return 0;
}

Here is the error message (gcc) if I uncomment the second call:

<source>: In function 'int main()':
<source>:62:46: error: no matching function for call to 'Convert2(const int&)'
   62 |     const std::optional<A> result2 = Convert2(in);
      |                                      ~~~~~~~~^~~~
<source>:30:20: note: candidate: 'template<class Out, class In> std::optional<_Tp> Convert2(const In&)'
   30 | std::optional<Out> Convert2(const In& p_in)
      |                    ^~~~~~~~
<source>:30:20: note:   template argument deduction/substitution failed:
<source>:62:46: note:   couldn't deduce template parameter 'Out'
   62 |     const std::optional<A> result2 = Convert2(in);
      |                                      ~~~~~~~~^~~~

Using auto, I was able to get this to work:

template<typename In>
auto Convert(const In& p_in)
{
    return std::nullopt;
}

template<>
auto Convert(const int& p_in)
{
    return std::make_optional<A>(p_in);
}

This has the advantage of being usable as I want at call site:

const std::optional<A> result4 = Convert3(in)

But I feel the function signature is now very much less informative on what the function does. One has to read the implementation to know a std::optional<A> is returned.

Would there be a way to make this work, without having to specify templated types at call site?

template<typename Out, typename In>
std::optional<Out> Convert(const In& p_in)
{
    // ?
}

template<>
std::optional<A> Convert(const int& p_in)
{
    // ?
}

int main()
{
    const std::optional<A> result = Convert(in)

    return 0;
}

Sources on Godbolt : https://godbolt.org/z/bYjrcPsPx.


Solution

  • The natural way to update the code to use std::optional would be:

    template <typename In, typename Out>
    bool OldConvert(const In& p_in, Out& p_out);
    
    template <typename Out, typename In>
    std::optional<Out> Convert(const In& p_in)
    {
        Out res;
        if (OldConvert(p_in, res)) { // Possibly put code directly here
            return std::make_optional(res);
        } else {
            return std::nullopt;
        }
    }
    

    and usage changes from

    A out;
    if (Convert(in, out)) {
        use(out);
    }
    

    to

    if (const auto out = Convert<A>(in)) {
        use(*out);
    }
    

    whereas auto might be textually replaced by std::optional<A> (if you want to be explicit) or std::optional (to avoid repetition of A)

    That seems idiomatic to me.

    You have another alternative with (ab)use of converting operator to have the syntax:

    if (const std::optional<A> out = Converter(in)) {
        use(*out);
    }
    

    with

    template <typename In>
    struct ConverterHelper
    {
        template <typename Out>
        operator std::optional<Out>() && {
            return Convert<Out>(arg);
        }
        const In& arg;
    };
    
    template <typename In>
    auto Converter(const In& p_in)
    {
        return ConverterHelper{p_in};
    }
    

    Demo