What's the preferred way of providing customization points for an API that contains templates?

58 Views Asked by At

Let's say I'm writing a library and at some point, there is code that wants to do something specific with objects of user-provided types. Take swap as an example, although the function could be arbitrary. What is important:

  • this should be static polymorphism (compile-time decision)
  • the library can provide a default implementation that is:
    • suitable for many cases (works with most types)
    • OR works with all types but may have a more efficient implementation for some
  • the user of the library should be able to replace this default implementation

There are 2 major ways of doing such thing in C++ at compile time:

  • function overloading
  • class template specialization

None of these on each own is ideal:

  • Function overloading is unsuitable in case the user-defined type is a class template. Function templates can not be partially specialized and having multiple function templates requires a lot of effort to maintain.
  • Class templates deal badly with trivial cases where overloading does perfectly fine.

Thus, my idea was - why not both? In fact, some STL functionality offers 2 or even more ways of customizing behavior. Typical STL algorithm offers 3:

  • providing additional function argument that will be used as the functor implementation (often a lambda)
  • overloading specific operator for specific type(s)
  • specializing specific class template that belongs to the standard library

In my case the first option (extra argument) is not possible because the supposed library does not immediately use that functor.

So I'm left with 2 possible ways of implementing overloading+specialization:

  • A) library calls function which's default overload calls primary class template (overloading is resolved first, then class template)
  • B) library calls primary class template (class template specialization resolved first) and primary class template implementation calls (potentially overloaded) function
// A

namespace lib {

template <typename T>
struct swapper // can be specialized
{
    void operator()(T& lhs, T& rhs) const noexcept
    {
        // default impl
    }
};

template <typename T>
void swap(T& lhs, T& rhs) noexcept // can be overloaded
{
    swapper<T>()(lhs, rhs);
}

template <typename T>
void core_function(/* ... */)
{
    T t1 = /* ... */;
    T t2 = /* ... */;

    /* ... */

    swap(t1, t2);
    
    /* ... */
}

}

// B

namespace lib {

template <typename T>
void swap(T& lhs, T& rhs) noexcept // can be overloaded
{
    // default impl
}

template <typename T>
struct swapper // can be specialized
{
    void operator()(T& lhs, T& rhs) const noexcept
    {
        swap(lhs, rhs);
    }
};

template <typename T>
void core_function(/* ... */)
{
    T t1 = /* ... */;
    T t2 = /* ... */;

    /* ... */

    swapper<T>()(t1, t2);
    
    /* ... */
}

}

// user code (the same for both)

namespace user {
    
struct user_type { /* ... */ };

// customization through overloading
void swap(user_type& lhs, user_type& rhs) noexcept
{
    // ...
}

// customization through class template specialization
template <>
struct swapper<user_type>
{
    void swap(user_type& lhs, user_type& rhs) const noexcept
    {
        // ...
    }
};

}

The queston is - which implementation is better - A or B? Does it make any difference in the order which of (function, primary class template) calls which? Are there any pros/cons of each of these 2 implementations?

0

There are 0 best solutions below