Good practices regarding template specialization a

2019-03-09 01:44发布

问题:

Template specialization does not take into account inheritance hierarchy. For example, if I specialize a template for Base and instantiate it with Derived, the specialization will not be chosen (see code (1) below).

This can be a major hindrance, because it sometimes lead to violation of the Liskov substitution principle. For instance, while working on this question, I noticed that I could not use Boost.Range algorithms with std::sub_match while I could with std::pair. Since sub_match inherits publicly from pair, common sense would dictate that I could substitute a sub_match everywhere a pair is used, but this fails due to trait classes using template specialization.

We can overcome this issue by using partial template specialization along with enable_if and is_base_of (see code (2)). Should I always favor this solution over full specialization, especially when writing library code? Are there any drawbacks to this approach that I have overseen? Is it a practice that you use or have seen used often?


Sample codes

(1)
#include <iostream>

struct Base {};
struct Derived : public Base {};

template < typename T >
struct Foo
{
    static void f() { std::cout << "Default" << std::endl; }
};

template <>
struct Foo< Base >
{
    static void f() { std::cout << "Base" << std::endl; }
};

int main()
{
    Foo<Derived>::f(); // prints "Default"
}

(2)
#include <type_traits>
#include <iostream>

struct Base {};
struct Derived : public Base {};

template <typename T, typename Enable = void>
struct Foo
{
    static void f() { std::cout << "Default" << std::endl; }
};

template <typename T>
struct Foo<
    T, typename 
    std::enable_if< std::is_base_of< Base, T >::value >::type
>
{
    static void f() { std::cout << "Base" << std::endl; }
};

int main()
{
    Foo<Derived>::f(); // prints "Base"
}

回答1:

enable_if is more flexible

I think you should really prefer the enable_if approach: it enables everything that you could require and more.

E.g. there might be cases where a Derived class is Liskov-Subsitutable for a Base, but you [cannot assume/donot want to apply] the same traits/specializations to be valid (e.g. because the Base is POD class, whereas Derived ands non-POD behaviour or somehting like that that is completely orthogonal to class composition).

enable_if gives you the power to define exactly the conditions.

Hybrid approach

You could also achieve some middleground by implementing a traits class that derives some application-specific traits from general-purpose traits. The 'custom' traits could use the enable_if and meta-programming techniques to apply traits as polymorphically as you desire. That way, your actual implementations do not have to repeat some complicated enable_if/dispatch dance but instead can simply consume the custom-traits class (that hides the complexity).

I think some (many?) Boost libraries use the hybrid approach (I've seen it in some capacity where it bridges e.g. fusion/mpl, I think also various iterator traits in Spirit).

I personally like this approach because it can effectively isolate the 'plumbing' from the core-business of a library, making maintenance and documentation (!) a lot easier.