简体   繁体   中英

How to efficiently initialize a std::variant data member in a class template

Consider the following class template, that can hold either a value of type T or an instance of some ErrorInfo class, using a std::variant data member:

template <typename T>
class ValueOrError
{
  private:
    std::variant<T, ErrorInfo> m_var;
};

How can I efficiently initialize the variant T alternative?

I can initialize it with a constructor like this:

template <typename T>
class ValueOrError
{
  public:
    explicit ValueOrError(const T& val) 
        : m_var{val} 
    {
    }

    …
};

But what syntax/coding technique can I use to enable move semantics optimization during initialization?

If I define a constructor taking a T&& , should I std::move or std::forward the parameter into the m_var ?

template <typename T>
class ValueOrError
{
  public:
    // Efficient initialization with move semantics 
    explicit ValueOrError(T&& val)
        : m_var{ /* ?? */ } 
    {
    }

    …
};

Note on interactions with ErrorInfo constructor overload

The ValueOrError template should also have a constructor overload that takes an ErrorInfo and initializes the variant member accordingly:

template <typename T>
class ValueOrError
{
  public:
    // Initialize with error code instead of T
    explicit ValueOrError(const ErrorInfo& error) 
        : m_var{error} 
    {
    }

    …
};

It's important that the generic T constructor overload interacts properly with the specific ErrorInfo overload.

ErrorInfo is a tiny class that wraps an error code (eg a simple integer), and can be constructed from such error code:

class ErrorInfo
{
  public:

    explicit ErrorInfo(int errorCode) 
        : m_errorCode{errorCode} 
    {
    }

    int ErrorCode() const 
    {
        return m_errorCode;
    }

    // … other convenient methods 
    // (e.g. get an error message, etc.)

  private:
    int m_errorCode;
};

A C++20 version using perfect forwarding :

#include <concepts> // std::constructible_from

template <class T>
class ValueOrError {
public:
    explicit ValueOrError(const ErrorInfo& error) : m_var{error} {}

    template<class... Args>
    requires std::constructible_from<T, Args...>
    explicit ValueOrError(Args&&... val) :
         m_var(std::in_place_type<T>, std::forward<Args>(val)...)
    {}

private:
    std::variant<T, ErrorInfo> m_var;
};

A C++17 version, also using perfect forwarding, could look like this:

#include <type_traits>  // std::is_constructible_v, std::enable_if_t

template <class T>
class ValueOrError {
public:
    explicit ValueOrError(const ErrorInfo& error) : m_var{error} {}

    template<class... Args,
             std::enable_if_t<std::is_constructible_v<T, Args...>, int> = 0>
    explicit ValueOrError(Args&&... val)
        : m_var(std::in_place_type<T>, std::forward<Args>(val)...) {}

private:
    std::variant<T, ErrorInfo> m_var;
};

Example usages:

class foo { // A non default constructible needing 3 constructor args
public:
    foo(double X, double Y, double Z) : x(X), y(Y), z(Z) {}
private:
    double x, y, z;
};

int main() {
    ValueOrError<foo> voe1(1., 2., 3.); // supply all three arguments

    // use the string constructor taking a `const char*`:   
    ValueOrError<std::string> voe2("Hello");

    std::string y = "world";
    // use the string constructor taking two iterators:
    ValueOrError<std::string> voe3(y.begin(), y.end());
}

I would do this this way in C++17 (using "perfect forwarding" + SFINAE):

template <typename T>
class ValueOrError
{
public:
    template<typename U>
    explicit ValueOrError(U&& val, std::enable_if_t<std::is_constructible_v<T, U>>* = nullptr)
    {
       m_var.template emplace<T>(std::forward<U>(val));
    }

private:
    std::variant<T, ErrorInfo> m_var = ErrorInfo{0};
};

Question is how this interact with constructors were error should be used?

Or initialization list version:

template <typename T>
class ValueOrError {
public:
    template <typename U>
    explicit ValueOrError(U&& val, std::enable_if_t<std::is_constructible_v<T, U>>* = nullptr)
        : m_var { std::in_place_type<T>, std::forward<U>(val) }
    {
    }

private:
    std::variant<T, ErrorInfo> m_var;
};

I have doubts if version with multiple arguments to construct T should be implemented. It is possible, but IMO will make code harder to read.

https://godbolt.org/z/scxacMn3W

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM