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 (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;
};
CodePudding user response:
A C 20 version using perfect forwarding:
#include <concepts> // std::constructible_from
template <typename T>
class ValueOrError {
public:
template<class... Args>
requires std::constructible_from<T, Args&&...>
explicit ValueOrError(Args&&... val) : m_var(T(std::forward<Args>(val)...)) {}
private:
std::variant<T, ErrorInfo> m_var;
};
Example usages:
int main() {
// use the constructor taking a `const char*`:
ValueOrError<std::string> voe1("Hello");
std::string y = "world";
// use the constructor taking two iterators:
ValueOrError<std::string> voe2(y.begin(), y.end());
}
CodePudding user response:
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.emplace<T>(std::forward<U>(val));
}
…
};
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.