I wrote a type trait template to test whether a type is a std::chrono::duration<Rep, Period>
. I was stuck for a long time figuring out how to deduce the Rep
and Period
.
I patterned this solution on a "possible implementation" given for a standard type trait on cppreference.com. I mostly understand how it works, but I'm confused by the ...
in the signature of the second test function.
#include <chrono>
#include <type_traits>
namespace detail {
template <typename T, typename Rep = T::rep, typename Period = T::period>
std::bool_constant<std::is_same_v<T, std::chrono::duration<Rep, Period>>> test();
template <typename>
std::false_type test(...); // How does `...` help here?
}
template <typename T>
struct is_duration : decltype(detail::test<std::remove_cv<T>::type>()) {};
template <typename T>
constexpr bool is_duration_v = is_duration<T>::value;
static_assert(is_duration_v<std::chrono::high_resolution_clock::duration>);
static_assert(is_duration_v<volatile std::chrono::microseconds>);
static_assert(is_duration_v<const std::chrono::microseconds>);
static_assert(!is_duration_v<double>);
ETA 2023-07-29
My question focused on the variadic argument part of the question, which was answered. But if you're here for the general type traits context ...
I voted to undelete Howard Hinnant's answer because it was extremely helpful for me to see a second way to solve the problem, so I believe it would help others as well.
I ended up defining a concept:
template <typename D, typename R=D::rep, typename P=D::period>
concept Duration = std::is_same_v<D, std::chrono::duration<R, P>>;
[Admittedly, that still relies on duration exposing its underlying types, but it makes the approach pretty clear. (And durations are documented to expose those types.) If you don't relish that, you could use Ted or Howard's example to make a type trait and then define the concept in terms of that.]
Overloads depending on the concept eliminated the clutter of enable-if
or requires
, yielding code that is easy to understand:
template <Duration T>
void Overloaded(T) { std::cout << "Called with a duration.\n"; }
template <std::floating_point T>
void Overloaded(T) { std::cout << "Called with a floating point value.\n"; }
template <typename T>
void Overloaded(T) { std::cout << "Called with a something else.\n"; }
does that ellipsis indicate a variadic argument list as it does in C?
Yes.
How does the ellipsis help distinguish that test from the other one?
It's simply always going to be the "least specialized" of the possible matching overloads, so, if there's another matching overload, that'll be the one chosen.
if I remove the ellipsis, the compiler reports that they're ambiguous.
Then both matched and you made them equally specialized.
Another way of testing would be to supply an instance of T
to test
and see in which test
it lands. test(...)
will accept anything and return a std::false_type
and any class templates taking two template parameters will be using the same condition as in your original. It could look like this:
template <class T>
struct is_duration {
static std::false_type test(...);
template <template <class, class> class D, class Rep, class Period>
static auto test(D<Rep, Period>&) -> std::bool_constant<
std::is_same_v<D<Rep, Period>, std::chrono::duration<Rep, Period>>>;
static constexpr bool value =
decltype(test(std::declval<std::remove_cvref_t<T>&>()))::value;
};