Elazar Leibovich
Elazar Leibovich

Reputation: 33623

Priority of template selection in C++

I just written the following piece of code

template<typename T>
inline bool contains(T haystack,typename T::key_type needle) {
    return haystack.find(needle) != haystack.end();
}

template<typename T>
inline bool contains(T haystack,typename T::value_type needle) {
    return find(haystack.begin(),haystack.end(),needle) != haystack.end();
}

When I'm instantiating the template with vector which doesn't have key_type typedef, SFINAE would make sure I wouldn't instantiate the first version. But what if I'm instantiating the template with a map, which have both key_type and value_type typedefs? How will the compiler choose which template function to use?

With current STL map, key_type is a pair, however what happens if I define a type where key_type is the same as value_type?

class MyMap {typedef int key_type;typedef int value_type;};
MyMap m;
contains(m,1); // both functions are valid, which will be chosen?

And surprise surprise, std::set has key_type == value_type. So I really need to resort to template meta programming in order to have a simple contains function. Sigh.

Bonus point for quoting the standard.

Upvotes: 6

Views: 1950

Answers (4)

If you are willing to do it, you can, with some metaprogramming help to aid you. Basically you would need to write a template metafunction that determines the condition under which you want to call one or the other functions, and use that in an enable_if_c clause:

template <typename T> inline
typename enable_if_c< has_key_type<T>::value, bool >::type
contains( T const & c, T::key_type const & k ) {...}        // associative container

template <typename T> inline
typename enable_if_c< !has_key_type<T>::value, bool >::type
contains( T const & c, T::value_type const & k ) {...}      // non-associative container

The enable_if_c template is a simple common SFINAE trick (that you can use from a C++0x compiler or boost). It takes a condition and a type, if the condition is true, it will generate an inner typedef to the argument type, if it is not present it will not define that inner type, and that can be used outside in SFINAE:

template <bool condition, typename T>
struct enable_if_c {};        // by default do not declare inner type

template <typename T>
struct enable_if_c<true,T> {  // if true, typedef the argument as inner type
    typedef T type;
};

Now the interesting part is how to determine that a type T has an inner type key_type, and while there might be other options, the first that comes to mind is:

template <typename T>
class has_key_type {
    typedef char _a;
    struct _b { _a x[2]; };

    template <typename U>
    static _a foo( U const &, typename U::key_type* p = 0 );
    static _b foo( ... );
    static T& generate_ref();
public:
    static const bool value = sizeof(foo(generate_ref())) == sizeof(_a);
};

The template has_key_type will contain an inner constant value that is true only if the type passed int contains an inner T::key_type type. The resolutions is not too complex: define a template function that will fail for all types but the one you want to detect, and provide a different overload with ellipsis so that it will catch if the template (has higher priority than the ellipsis) fails to substitute. Then use the size of the return types to detect which overload was chosen by the compiler. The generate_ref is there to avoid having to actually construct an object (i.e. do not impose that T can be constructed in any specific way).

The overall result is that when the type T contains an inner type key_type, the result of has_key_type<T>::value will be true, and the enable_if_c will enable the first overload and disable the second, so SFINAE will discard the second overload not because of the type second function argument not being defined, but rather on terms of the return type.

If you think of it, there is just a bunch of boiler plate code around a small change: instead of providing two ambiguous template function overloads, provide a template overload and a lesser priority function (ellipsis). Since you cannot really use that ellipsis function to implement the non-associative container version, it is used to obtain a boolean value that is then seeded into common metaprogramming structures and boilerplate.

Upvotes: 3

Bo Persson
Bo Persson

Reputation: 92301

Using it on a map seems to work.

However, if you were to use it on some container where key_type and value_type were the same type, this would end up defining "the same" function twice (and differently on top of that).

§13.1

Certain function declarations cannot be overloaded:
...
— Parameter declarations that differ only in the use of equivalent typedef “types” are equivalent. A typedef is not a separate type, but only a synonym for another type (7.1.3).
[ Example:
typedef int Int;
void f(int i);
void f(Int i); // OK: redeclaration of f(int)
void f(int i) { /* ... */ }
void f(Int i) { /* ... */ } // error: redefinition of f(int)
—end example ]

Upvotes: 0

otto
otto

Reputation: 1158

If there are two or more equally good candidates for function template specialization, you will get a compiler error.

C++ standard says:

If there is exactly one viable function that is a better function than all other viable functions, then it is the one selected by overload resolution; otherwise the call is ill-formed12).

Upvotes: 2

Erik
Erik

Reputation: 91290

The key_type template is a match, the value_type template isn't.

map<int,int>::value_type is a pair<const int,int> - so only your first template will match. If your second template used e.g. mapped_type you'd have a compiler error due to ambiguity.

Upvotes: 2

Related Questions