Check traits for all variadic template arguments
Asked Answered
F

2

18

Background : I've created the following class C, whose constructor should take N variables of type B& :

class A;
class B
{
    A* getA();
};

template<size_t N>
class C
{
public:
    template<typename... Args>
    inline C(Args&... args) :
        member{args.getA()...}
    {}
private:
    std::array<A*, N> member;
};

Problem : my problem is how to constraint the variadic Args to be all of type B ?

My partial solution : I wanted to define a predicate like :

template <typename T, size_t N, typename... Args>
struct is_range_of :
    std::true_type // if Args is N copies of T
    std::false_type // otherwise
{};

And redefine my constructor accordingly :

template <typename... Args,
          typename = typename std::enable_if<is_range_of_<B, N, Args...>::value>::type
         >
inline C(Args&... args);

I've seen a possible solution on this post : https://stackoverflow.com/a/11414631, which defines a generic check_all predicate :

template <template<typename> class Trait, typename... Args>
struct check_all :
    std::false_type
{};

template <template<typename> class Trait>
struct check_all<Trait> :
    std::true_type
{};

template <template<typename> class Trait, typename T, typename... Args>
struct check_all<Trait, T, Args...> :
    std::integral_constant<bool, Trait<T>::value && check_all<Trait, Args...>::value>
{};

So, I could write something like :

template <typename T, size_t N, typename... Args>
struct is_range_of :
    std::integral_constant<bool,
        sizeof...(Args) == N &&
        check_all<Trait, Args...>::value
    >
{};

Question 1 : I don't know how to define the Trait, because I need somehow to bind std::is_same with B as first argument. Is there any means of using the generic check_all in my case, or is the current grammar of C++ incompatible ?

Question 2 : My constructor should also accept derived classes of B (through a reference to B), is it a problem for template argument deduction ? I am afraid that if I use a predicate like std::is_base_of, I will get a different instantiation of the constructor for each set of parameters, which could increase compiled code size...

Edit : For example, I have B1 and B2 that inherits from B, I call C<2>(b1, b1) and C<2>(b1, b2) in my code, will it create two instances (of C<2>::C<B1, B1> and C<2>::C<B1, B2>) ? I want only instances of C<2>::C<B, B>.

Fateful answered 31/1, 2015 at 15:41 Comment(4)
Do you want them to be derived from B, or just implicitly convertible to B?Ada
I want them to be derived from B, see my edit. I need a template generalization to N arguments of a class that defines the constructor C(B& b).Fateful
The arguments being derived from B is a both a stronger and a weaker constraint than being convertible. As neither implies the other.Ada
They'd better hurry up with those concepts stuff =)Mccutcheon
L
39

Define all_true as

template <bool...> struct bool_pack;

template <bool... v>
using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>;

And rewrite your constructor to

// Check convertibility to B&; also, use the fact that getA() is non-const
template<typename... Args,
       typename = std::enable_if_t<all_true<std::is_convertible<Args&, B&>{}...>>
C(Args&... args) :
    member{args.getA()...}
{}

Alternatively, under C++17,

template<typename... Args,
       typename = std::enable_if_t<(std::is_convertible_v<Args&, B&> && ...)>>
C(Args&... args) :
    member{args.getA()...}
{}

I am afraid that if I use a predicate like std::is_base_of, I will get a different instantiation of the constructor for each set of parameters, which could increase compiled code size...

enable_if_t<…> will always yield the type void (with only one template argument given), so this cannot be is_base_ofs fault. However, when Args has different types, i.e. the types of the arguments are distinct, then subsequently different specializations will be instantiated. I would expect a compiler to optimize here though.


If you want the constructor to take precisely N arguments, you can use a somewhat easier method. Define

template <std::size_t, typename T>
using ignore_val = T;

And now partially specialize C as

// Unused primary template
template <size_t N, typename=std::make_index_sequence<N>> class C;
// Partial specialization
template <size_t N, std::size_t... indices>
class C<N, std::index_sequence<indices...>>
{ /* … */ };

The definition of the constructor inside the partial specialization now becomes trivial

C(ignore_val<indices, B&>... args) :
    member{args.getA()...}
{}

Also, you do not have to worry about a ton of specializations anymore.

Lynnett answered 31/1, 2015 at 15:52 Comment(13)
Heh, all_true is definitely nicer than recursion. Not sure about using is_base_of, though, since that accepts inaccessible or ambiguous bases as well.Vivi
@Vivi I think this isn't the optimal approach anyway. But how can is_base_of yield true for inaccessible bases? I thought it checks whether the derived-to-base conversion is applicable?Lynnett
Nice trick for the all_true and thanks for the C++14 syntax !Fateful
Why do you decay the types?Ada
@Ada I was thinking of forwarding references and cv-qualifiers.Lynnett
@Lynnett There's a nice trick that allows you to do it in code: https://mcmap.net/q/186601/-how-does-this-implementation-of-the-is_base_of-trait-work; but an intrinsic works too. Up to the implementation, I guess.Vivi
@Fateful I did add another method that might be more suitable for you.Lynnett
@Lynnett Thanks for the index_sequence trick. Will these indices introduce any tradeoff in the compiled code ? Otherwise this is exactly what I need !Fateful
@Fateful index_sequence is evaluated at compile time, so I believe that shouldn't affect the produced code in performance.Lynnett
@Lynnett Using is_convertible on references permits user-defined conversions. Use it on pointers.Vivi
@Vivi I did originally, before editing. However, the semantics of the constructor shouldn't necessarily be excluding user-defined conversions, should they? Compare with the second method?Lynnett
@Lynnett Well, the question is framed as B and classes derived from it. Whether it's desired to allow user-defined conversions probably depends on the use case.Vivi
The fold expression of the C++17 version requires parentheses: template<typename... Args, typename = std::enable_if_t<(std::is_convertible_v<Args&, B&> &&...)>>. I cannot edit only two characters.Safekeeping
A
1
namespace detail {
    template <bool...> struct bool_pack;
    template <bool... v>
    using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>;
    template<class X> constexpr X implicit_cast(std::enable_if_t<true, X> x) {return x;}
};

The implicit_cast is also in Boost, the bool_pack stolen from Columbo.

// Only callable with static argument-types `B&`, uses SFINAE
template<typename... ARGS, typename = std::enable_if_t<
    detail::all_true<std::is_same<B, ARGS>...>>>
C(ARGS&... args) noexcept : member{args.getA()...} {}

Option one, if it's implicitly convertible that's good enough

template<typename... ARGS, typename = std::enable_if_t<
    detail::all_true<!std::is_same<
    decltype(detail::implicit_cast<B&>(std::declval<ARGS&>())), ARGS&>...>>
C(ARGS&... args) noexcept(noexcept(implicit_cast<B&>(args)...))
    : C(implicit_cast<B&>(args)...) {}

Option two, only if they are publicly derived from B and unambiguously convertible:

// Otherwise, convert to base and delegate
template<typename... ARGS, typename = decltype(
    detail::implicit_cast<B*>(std::declval<ARGS*>())..., void())>
C(ARGS&... args) noexcept : C(implicit_cast<B&>(args)...) {}

The unnamed ctor-template-argument-type is void in any successful substitution.

Ada answered 31/1, 2015 at 16:50 Comment(5)
So why are you decaying Args?Lynnett
By the way, why not just use enable_if_t<std::is_convertible<Args*, B*>{}>?Lynnett
The remove_reference_t is superfluous AFAICS. Args can never be deduced to a reference type, that is only possible for forwarding references.Lynnett
@Ada : this implementation will generate several instantiations when used with different types, right ? (though they all delegate to the first constructor)Fateful
@Steakfly: Those many instantiations of the second template (whichever second one you might choose) are unavoidable. But they are all utterly trivial forwarders, and if they aren't outright collapsed with the forwarded-to constructor, they will certainly be inlined and eliminated without trace anyway. Also added noexcept-specifier.Ada

© 2022 - 2024 — McMap. All rights reserved.