Home > database >  How do I define a function that takes a variadic class template?
How do I define a function that takes a variadic class template?

Time:09-17

I am trying to define a simple variant-based Result type alias, sort of like a poor man's rust-like Result type :

namespace detail {
template <typename SuccessType, typename... ErrorTypes>
struct Result {
  using type = std::variant<SuccessType, ErrorTypes...>;
};

template <typename... ErrorTypes>
struct Result<void, ErrorTypes...> {
  using type = std::variant<std::monostate, ErrorTypes...>;
};
}  // namespace detail

template <typename SuccessType, typename... ErrorTypes>
using Result_t = detail::Result<SuccessType, ErrorTypes...>::type;

i.e. a Result_t is just an std::variant where the 0th index is the successful result and the rest are error structs.

I defined this helper method to check if the result is good:

template <typename SuccessType, typename... ErrorTypes>
inline bool Ok(const Result_t<SuccessType, ErrorTypes...>& r) {
  return r.index() == 0;
}

But I get a "no matching overloaded function found" when I try to instantiate it:

error C2672: 'Ok': no matching overloaded function found

error C2783: 'bool Ok(const detail::Result<SuccessType,ErrorTypes...>::type &)': could not deduce template argument for 'SuccessType'

struct FileError {};
struct BadJson {};

template <typename T>
using Result = Result_t<T, FileError, BadJson>;

Result<void> GetVoid() { return {}; }

TEST(ConfigFileTest, Result) {
  auto res = GetVoid();
  EXPECT_EQ(res.index(), 0);
  
  bool ok = Ok(res);
  EXPECT_TRUE(ok);
}

What am I doing wrong? If I just have Ok be templated like template <typename T> Ok(const T& r) it works, but makes the function too general.

CodePudding user response:

After expanding the Result_t alias in the function parameter, it looks like this:

template <typename SuccessType, typename... ErrorTypes>
bool Ok(const detail::Result<SuccessType, ErrorTypes...>::type& r) {
  return r.index() == 0;
}

The problematic part here is that the template parameters are left of the name resolution operator ::. Everything left of :: is a non-deduced context, meaning that it is not used to deduce template arguments. So since SuccessType and ErrorTypes... appear only in non-deduced context, they cannot be deduced and a call which doesn't explicitly specifies them will fail.

You can see that this rule is necessary, because theoretically any specialization of detail::Result<SuccessType, ErrorTypes...> could have a ::type that matches the arguments type. There is no way that the compiler can check this for every possible combination of types.

Instead of trying to alias types, make Result an actual new type:

template <typename SuccessType, typename... ErrorTypes>
struct Result {
  using variant_type = std::variant<SuccessType, ErrorTypes...>;
  variant_type variant;
};

template <typename... ErrorTypes>
struct Result<void, ErrorTypes...> {
  using variant_type = std::variant<std::monostate, ErrorTypes...>;
  variant_type variant;
};

template <typename SuccessType, typename... ErrorTypes>
bool Ok(const Result<SuccessType, ErrorTypes...>& r) {
  return r.variant.index() == 0;
}

or something along those lines. If you really want to use the old design using only aliases, then the function should not take the nested alias as argument, but the actual type instead (which is probably not match the intent of the design):

template <typename T, typename... ErrorTypes>
bool Ok(const std::variant<T, ErrorTypes...>& r) {
  return r.index() == 0;
}

(I removed the inline on the templates. inline on a function template doesn't really make much sense.)

  • Related