Search code examples
c++argument-dependent-lookup

Customization points and ADL


I am writing a library and there is a function that performs an (unqualified) call to free function foo using an arbitrary type as argument:

namespace lib {

template <typename T>
auto libfunc(T && t)
{
  return foo(std::forward<T>(t));
}

} // namespace lib

A user of the library can write an overload of foo for his own types:

namespace app {

class Foo { };

template <typename> class Bar { };

int foo(Foo const &) { return 99; }

template <typename T>
char foo(Bar<T> const &) { return 'x'; }

} // namespace app

The correct funtion foo is found by ADL so code like this works:

app::Foo foo;
app::Bar<void**> bar;

auto x = lib::libfunc(foo);
auto y = lib::libfunc(bar);

However, if I want to write a version of foo that works for types from the std-namespace, no matching function foo is found unless I place foo in the std-namespace which is not allowed:

#ifdef EVIL
namespace std {
#endif

template <typename T>
double foo(std::vector<T> const & ) { return 1.23; }

#ifdef EVIL
} // namespace std
#endif

std::vector<int> vec;

lib::libfunc(vec); // Only works if EVIL is defined

Is it possible to change the code so that a user can enable the functionality foo for a type without invading its namespace? I thought about partial template specializations of a class template in the lib-namespace but is there any other possibility?


Solution

  • I've found two solutions to this problem. Both have their downsides.

    Declare All Std Overloads

    Let overloads for standard types be found by normal lookup. This basically means declaring all of them before using the extension function. Remember: when you perform an unqualified call in a function template, normal lookup happens at the point of definition, while ADL happens at the point of instantiation. This means that normal lookup only finds overloads visible from where the template is written, whereas ADL finds stuff defined later on.

    The upside of this approach is that nothing changes for the user when writing his own functions.

    The downside is that you have to include the header of every standard type you want to provide an overload for, and provide that overload, in the header that just wants to define the extension point. This can mean a very heavy dependency.

    Add Another Argument

    The other option is to pass a second argument to the function. Here's how this works:

    namespace your_stuff {
        namespace adl {
            struct tag {}
    
            void extension_point() = delete; // this is just a dummy
        }
    
        template <typename T>
        void use_extension_point(const T& t) {
            using adl::extension_point;
            extension_point(t, adl::tag{}); // this is the ADL call
        }
    
        template <typename T>
        void needs_extension_point(const T& t) {
            your_stuff::use_extension_point(t); // suppress ADL
        }
    }
    

    Now you can, at basically any point in the program, provide overloads for std (or even global or built-in) types like this:

    namespace your_stuff { namespace adl {
        void extension_point(const std::string& s, tag) {
            // do stuff here
        }
        void extension_point(int i, tag) {
            // do stuff here
        }
    }}
    

    The user can, for his own types, write overloads like this:

    namespace user_stuff {
        void extension_point(const user_type& u, your_stuff::adl::tag) {
            // do stuff here
        }
    }
    

    Upside: Works.

    Downside: the user must add the your_stuff::adl::tag argument to his overloads. This will be probably seen as annoying boilerplate by many, and more importantly, can lead to the big puzzling "why doesn't it find my overload" problem when the user forgets to add the argument. On the other hand, the argument also clearly identifies the overloads as fulfilling a contract (being an extension point), which could be important when the next programmer comes along and renames the function to extensionPoint (to conform with naming conventions) and then freaks out when things don't compile anymore.