Potatoswatter
Potatoswatter

Reputation: 137810

Inheriting (or member) traits idiom

Catch-all traits classes like std::iterator_traits are useful by separating the properties of a type from its definition, so for example the properties may be made available before the definition is complete.

Defining traits in addition to each client class itself is inconvenient, because the traits typically also have a place as members. This is why the generic implementation of std::iterator_traits is defined in terms of its template argument's members.

template< typename it >
struct iterator_traits {
    typedef typename it::category category;
    typedef typename it::value_type value_type;
    // etc
};

Isn't it easier, and less work for the compiler, to use inheritance instead?

template< typename t >
struct t_traits : public t {
    t_traits() = delete; // Prevent runtime instances.
};

This fails to document the interface in the primary template, but there are other opportunities for that anyway.

It seems pointless to write lots of repetitive code to define a meta-container class, in an idiom which doesn't even guarantee prevent such abuse as creation at runtime.


Or maybe that's entirely backwards. In addition to std::iterator_traits we also have std::iterator, a pseudo-abstract base class with mostly the same members. Such redundancy is a code smell. Wouldn't it be better if custom iterators looked like this?

template<>
struct iterator_traits< struct my_iterator > {
    typedef random_access_iterator_tag category;
    typedef foo value_type;
    ...
};
struct my_iterator : iterator_traits< struct my_iterator > {
    ...
};

(For the sake of argument, let's ignore the fact that an actual std::iterator_traits specialization must be declared in namespace std. I'm trying to make a familiar illustration of something that might happen in user code.)

This is cleaner in that the idiom need not be violated to handle whatever exceptional case necessitated the fancy footwork in the first place. Instead of the primary traits template producing an internal error that the missing client class is unsuitable for something, there need not be any primary traits template at all.

It's conceptually better to separate qualities of a class from implementation of its services, regardless of whether that separation is necessary. BUT, this style does require breaking every client class into two pieces, including an explicit specialization, which is sort of ugly.


Is anyone familiar with this design space? I'm leaning toward the second idiom, although it looks unusual in practice. But there are probably ins and outs known to those who have trod here before.

Upvotes: 3

Views: 279

Answers (1)

Potatoswatter
Potatoswatter

Reputation: 137810

The problem with user-defined traits as a specialization of a library type is that a library type belongs to the library. Defining an explicit specialization requires opening the library namespace, which is ugly.

Alternatives 1 and 2 can be combined into a best of both worlds pattern that

  • always allows optimal separation of concerns (by splitting a class into traits and implementation)
  • doesn't require splitting a class
  • never requires opening the library namespace

An extra bit of glue is needed in the form of an ADL based metafunction mapping any class to its traits.

template< typename t >
t traits_type_entry( t const & ); // Declared, never defined.

template< typename t >
using traits_type = decltype( traits_type_entry( std::declval< t >() ) );

By default, T serves as its own traits type as traits_type< T >::type is T. To change this for a given type t to a traits class t_traits, declare (but do not define) a function t_traits traits_type_entry( t const & ). This t_traits class may or may not be a base class of t; the traits_type facility doesn't care. Because the function will be found by argument-depenedent lookup, it may be declared as a friend function with no declaration at namespace scope.

Usage nested inside a class (just to make a difficult test-case) looks like this. For usual usage in a namespace just drop the friend keyword.

class outer_scope {
    struct special;
    struct special_traits {
        typedef int value_type;
        constexpr static int limit = 5;
    };
    friend special_traits traits_type_entry( special const & );

    struct unspecial {
        typedef double baz_type;
        int table[ util::traits_type< special >::limit ];
    };

    struct special : special_traits {
        void f() {
             std::pair< typename util::traits_type< unspecial >::baz_type,
                        value_type >();
        }
    };
};

http://ideone.com/QztQ6i

Note, the t const & parameter to traits_type_entry can be simply t as long as the class is copyable and destructible.

Also, you could prevent declaring an object of (non-customized) trait type by having the primary template return a type derived from t with its constructor deleted, instead of t itself.

Upvotes: 1

Related Questions