Search code examples
c++templatesenable-if

Getting overload selection right with template template params and enable_if


I've got a template function in VS2013 designed to perform a "deep copy" of whatever object given to it. One overload is for trivial types just calls operator=. But I also have an overload designed to work for vectors of shared_ptr to my own Shape-class objects, which can only be duplicated by calling a clone() member function.

struct Shape { virtual std::shared_ptr<Shape> clone() const = 0; };
struct Rectangle : public Shape { virtual std::shared_ptr<Shape> clone() const override; };

So I've got this overload and the compiler chooses it just fine

template<class SHP> 
inline std::vector<std::shared_ptr<SHP>> deep_copy(
    const std::vector<std::shared_ptr<SHP>>& val,    
    typename std::enable_if<std::is_base_of<Shape, SHP>::value>::type** = nullptr)
{
    // ... blah blah blah
}

std::vector<std::shared_ptr<Rectangle>> objects;
auto objects2 = deep_copy(objects);

Then I wanted to change this to take ANY non-keyed collection (e.g. list) of shared_ptr. OK, no problem, I actually managed that...

template<class COLL> 
inline COLL deep_copy(const COLL& val,
    typename std::enable_if<std::is_base_of<Shape, typename COLL::value_type::element_type>::value>::type** = nullptr)

But this syntax doesn't really ensure that the collection contains shared_ptr. It merely ensures that its value_type has a nested element_type that is some sort of Shape

So my question is, What would be the syntax for ensuring that the contents of the collection are actually std::shared_ptr to something derived from Shape?

I've made serveral attempts at this using template template parameters but I keep screwing it up.


Solution

  • Let's start with a concept for testing if a T is a shared_ptr:

    template<typename T>
    struct is_shared_ptr : std::false_type{};
    
    template<typename T>
    struct is_shared_ptr<std::shared_ptr<T>> : std::true_type{};
    

    Now something like

    std::cout << std::boolalpha << is_shared_ptr<std::shared_ptr<Shape>>::value << std::endl;
    

    Will output

    true

    Next, if something gives a true_type as a result of using that concept, we'd want to next check its element type:

    template<typename T>
    using element_t = typename std::decay_t<T>::element_type;
    

    To see if the returned type derives from Shape.

    Let's create another helper alias to grab the value_type off a collection:

    template<typename T>
    using collection_vt = typename std::decay_t<T>::value_type;
    

    And now we can combine these concepts into one really gross looking one:

    template<template<class...> class C, typename U, typename... A>
    auto DoTheThing(const C<U, A...>& _collection) -> std::enable_if_t<
    is_shared_ptr<collection_vt<decltype(_collection)>>::value && 
    std::is_base_of<Shape, element_t<collection_vt<decltype(_collection)>>>::value
    >
    {
        std::cout << _collection.size() << std::endl;
    }
    

    The initial template arguments are for accepting a relatively generic container. Then we use a trailing return type to first ensure that value_type is a shared_ptr, and then afterwards also check that element_type of the shared_ptr derives from Shape. Since, upon success, the type from enable_if is void, the return type of the function becomes void.

    Live Demo

    Here's the test:

    std::vector<std::shared_ptr<Shape>> vec1; 
    DoTheThing(vec1); // success
    
    std::vector<Wrapper<Shape>> vec2;
    //DoTheThing(vec2); // error
    
    std::vector<std::shared_ptr<int>> vec3;
    //DoTheThing(vec3); // error
    
    std::list<std::shared_ptr<Shape>> vec4;
    DoTheThing(vec4); // success