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{ /* ?? */ }
{
}
…
};
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 (e.g. 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());
}