Thursday, July 21, 2011

Improving conversions of std::pair<T,U>s

As we already saw, even if the implementations are more permisive, the standard mandates that conversions from std::pair<> types should use only implicit conversions. That solves only half of the problem: when two types are implicitly convertible, then std::pair<> containing those types are also implicitly convertible.

But what about explicit conversions? Do we need to fail there? It would seem appropriate if, given two types that are explicitly convertible, we allowed explicit conversions of the std::pair.

From here on, we digress from the standard. This is just a simple example of how to use SFINAE(1). As this is the first post with SFINAE, it will be longer than I expected.

The problem

What we would like is to allow implicit conversions of pairs of different types when the types are implicitly convertible pairwise, and also allow explicit conversions of pair when the respective types are not implicitly convertible. If only one of the types can be implicitly converted, we will require an explicit conversion for the pair

The solution

The solution for the problem is providing two different constructor templates that allow for the conversion. The first one of them will be implicit and will only work for types that are implicitly convertible, while the second one will be marked explicit and will be available whenever the first one is not. If both where available for the same combination of types, the compiler would fail to process the call with an ambiguity error. Because SFINAE requires that the error/failure is during the substitution phase, we will need to change the signatures of the constructors, but we will aim to maintain compatibility of user code.

Detect implicit convertibility

This is probably the simplest part of the problem. We need a template with two type arguments, that offers a boolean constant true when the types are implicitly convertible.
template <typename From, typename To>
class implicit_convertible {
    typedef char (&yes)[1];
    typedef char (&no)[2];
    static yes test( To );
    static no test( ... );
public:
    static const bool value 
           = sizeof( test( *(From*)0 ) ) == sizeof( yes );
};
We create two types of different sizes yes and no, and we define two static functions, the first of which takes an argument of the destination type. The second is an ellipsis catch-all. We ask the compiler to perform overload resolution for an object of type From(2) and we check whether the first overload was chosen.

Implementing SFINAE

For SFINAE to work, the compiler must be able to infer the types of the template from the arguments, but when substituting the inferred types in the templates, the compiler must be unable to produce a correct signature. As a helper we can use a variant of enable_if:
template <bool enabled, typename T = void>
struct enable_if {};

template <typename T>
struct enable_if<true,T> {
   typedef T type;
};
The template is specialized for the case where the first argument is true, in which case it defines an internal type type. If the first argument is false the type is an empty class. Now we can use this to define our complete solution:

Implementation

template <typename T1, typename T2>
struct pair {
   T1 first;
   T2 second;
   pair() {}
   pair( T1 f, T2 s ) : first(f), second(s) {}
   
   template<typename U, typename V>
   pair( pair<U,V> const & p, 
         typename enable_if< implicitly_convertible<U,T1>::value
                         and implicitly_convertible<V,T2>::value 
                           >::type* p = 0 )
      : first( p.first ), second( p.second )
   {}

   template<typename U, typename V>
   explicit pair( pair<U,V> const & p, 
                  typename enable_if< !implicitly_convertible<U,T1>::value
                                   or !implicitly_convertible<V,T2>::value
                                    >::type* p = 0 )
      : first( p.first ), second( p.second )
   {}
};
To be able to use SFINAE we have added an extra argument that uses the enable_if template, if the condition is not met, then typename enable_if< false >::type will not resolve to a type, and the compiler will discard that overload. It is important to note that the conditions must be mutually exclusive, else the compiler will generate an ambiguity error.

If instead of a constructor we were applying SFINAE to a regular function, we could have used enable_if in the return type, and the signature of the function after substitution would have remained unmodified. In the case of a constructor that is not an option, and we were forced to add the extra argument. By making it a pointer and providing a default value we manage to maintain user code compatibility with std::pair, but we accept calls to the constructor with an extra void* argument. Probably not an issue.

(1)Substitution Failure Is Not An Error. That weird acronym stands for the fact that, after lookup is performed and a template is determined to be a candidate for overload resolution if the substitution of the inferred types in the template fails, the compiler will discard that particular template and continue trying the rest of the overload candidates without triggering an error.

(2)To avoid imposing arbitrary requirements in the type, we create a pointer of the source type initialized to 0 and we dereference it. This is undefined behavior in real code, but because we are using the whole expression inside a sizeof the expression is not evaluated, it is only used to extract the type From& so we are fine.

No comments:

Post a Comment