简体   繁体   中英

Inheritance or composition when recursing over a parameter pack?

When recursing over a parameter pack, given the choice, should I prefer to recurse via inheritance, or via a member field (composition)? Is this a cut-and-dried matter? What are the trade-offs?

One thing that I'm wondering, for example, is whether the composition-based form is generally considered to have better compilation speed, memory usage, or error reporting.

To illustrate, the following is an example of short-circuit or_ (disjunction) inspired by Jonathan Wakely's answer here .

Inheritance-based:

#include <type_traits>

// disjunction

template<typename... Conds>
struct or_ : std::false_type {};

template<typename Cond, typename... Conds>
struct or_<Cond, Conds...> 
  : std::conditional<Cond::value, std::true_type, or_<Conds...>>::type
{};

static_assert(or_<std::true_type, std::true_type, std::true_type>::value,"");
static_assert(or_<std::false_type, std::false_type, std::true_type>::value,"");
static_assert(or_<std::false_type, std::false_type, std::false_type>::value == false,"");

I understand that this version has the feature that or_<Ts...> will inherit from std::integral_constant . Please assume for the sake of my question that I don't care about whether or_ inherits from an integral_constant .

Composition-based:

template<typename... Conds>
struct or_ {
    static constexpr bool value = false;
};

template<typename Cond, typename... Conds>
struct or_<Cond, Conds...> {
    static constexpr bool value = std::conditional<Cond::value, std::true_type, or_<Conds...>>::type::value;
};

This form seems intuitively better to me, because the value is always located in the type itself, not in some superclass, but I'm not sure whether this is generally considered preferable.

PS I know that in C++17 I could often use fold expressions. But I'm aiming for C++11 compatibility.

If we really don't care about short-circuiting, then there's always bool_pack :

template<bool...> struct bool_pack;

template<class... Ts>
using and_ = typename std::is_same<bool_pack<true, Ts::value...>,
                                   bool_pack<Ts::value..., true>>::type;

template<class T>
using not_ = std::integral_constant<bool, !T::value>;

template<class... Ts>
using or_ = not_<std::is_same<bool_pack<false, Ts::value...>, 
                              bool_pack<Ts::value..., false>>>; 
      // or not_<and_<not_<Ts>...>>

Both forms would give similar effect. If you want to make the code better instead of choosing between those two you could try to avoid recursion... Your or_ code could be successfully replaced with the following:

#include <utility>
#include <type_traits>
#include <iostream>

template <std::size_t, class T>
struct indexed_condition;

template <std::size_t I>
struct indexed_condition<I, std::false_type> { };

template <class...>
struct voider {
    using type = void;
};

template <class... Conds>
struct helper: std::true_type {
};

template <std::size_t... Is, class... Conds>
struct helper<typename voider<decltype(indexed_condition<Is, Conds>())...>::type, std::index_sequence<Is...>, Conds...>: std::false_type {
};

template <class... Conds>
struct my_or: helper<void, std::make_index_sequence<sizeof...(Conds)>, Conds...> { };

int main() {
    std::cout << my_or<std::true_type, std::false_type, std::true_type>::value << std::endl;
    std::cout << my_or<std::false_type, std::false_type, std::false_type>::value << std::endl;
}

[live demo]

Output:

1
0

To be straight it won't affect run-time but the compilation-time efficiency.

One more thing - std::integer_sequence is c++14 construction, but can be implemented in c++11 as well see eg this implementation

This is an attempt to make a slightly cleaner version of @WF's answer.

template<std::size_t, class T, class=void>
struct detect_truthy : virtual std::false_type {};
template<std::size_t I, class T>
struct detect_truthy<I, T,
  typename std::enable_if<T::value>::type
> : virtual std::true_type {};

template<class...>struct pack {};

template<class Pack, class Is=void>
struct detect_truthies;
template<class...Ts>
struct detect_truthies<pack<Ts...>,void>:
  detect_truthies<pack<Ts...>, std::make_index_sequence<sizeof...(Ts)>>
{};
template<class...Ts, std::size_t...Is>
struct detect_truthies<pack<Ts...>,std::index_sequence<Is...>>:
  detect_truthy<Is, Ts>...
{};

std::true_type or_helper_f( std::true_type* );
std::false_type or_helper_f( ... );
std::false_type and_helper_f( std::false_type* );
std::true_type and_helper_f( ... );

template<class...Bools>
struct my_or:
  decltype( or_helper_f( (detect_truthies<pack<Bools...>>*)0 ) )
{};
template<class...Bools>
struct my_and:
  decltype( and_helper_f( (detect_truthies<pack<Bools...>>*)0 ) )
{};

live example .

It uses index_sequence and make_index_sequence , which is from C++14, but high quality C++11 versions can be used.

The point of this technique is that the only recursive/linear/binary template expansion occurs within index_sequence . Everywhere else we directly expand parameter packs.

As index_sequence can be written to be highly efficient at the cost of readability (for example, MSVC implements it as an intrinsic! Other compilers use a complex binary tree generator.), this focuses the high cost performance issues there.

In general, you want to isolate the spots where you do O(N^2) work at compile time. When you inherit linearly, you do O(N^2) work.

There's a third option which is to use constexpr functions

static constexpr bool any_of(){
  return false;
}

template<class... T>
static constexpr bool any_of(bool b, T... rest){
  return b || any_of(rest...);
}


template<class... Cond>
using or_ = std::integral_constant<bool, any_of(Cond::value...)>;

int main(){
  static_assert(or_<std::true_type, std::true_type, std::true_type>::value,"");
  static_assert(or_<std::false_type, std::false_type, std::true_type>::value,"");
  static_assert(or_<std::false_type, std::false_type, std::false_type>::value == false,"");
}

The advantage of the inheritance method is that you can pass the _or class to any function that is overloaded with both a std::true_type and a std::false_type, which helps if you're playing with sfinae.

As far as compilation speed goes it's very hard to tell because it's completely compiler dependent (and the compiler guys are working on compilation speed for TMP so any good advice now might turn into bad advice without warning.

The main thing with compilation speed is to keep an eye on your algorithms. It's very hard to do conditional compilation in C++ 11/14 and compilers will evaluate both sides of a std::conditional. (They have to you can legally specialise std::conditional for one of the _or classes and the compiler can't prove you're a rational human being)

This means in both your cases the compiler will instantiate the or class for every partial list of cases. This probably isn't too bad, but sometimes you can suddenly turn an O(N) algorithm into an O(2^N) algorithm, which is when compilation times really start to hurt.

One more quite efficient in context of compilation time approach to or_ :

#include <type_traits>
#include <utility>

template <class... Conds>
std::false_type or_impl(Conds... conds);
template <class... Conds>
std::true_type or_impl(...);

template <class, class T>
using typer = T;

template <class... Conds>
using or_ = decltype(or_impl<typer<Conds, std::false_type>...>(Conds{}...));

int main() {
    static_assert(or_<std::false_type, std::true_type, std::false_type>::value, "!");
    static_assert(!or_<std::false_type, std::false_type, std::false_type>::value, "!");
}

[live demo]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM