Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simulating argument-dependent lookup for template arguments

I've encountered this problem while writing some library-like code recently, and I thought discussing it might help others as well.

Suppose I have a library with some function templates defined in a namespace. The function templates work on types supplied by client code, and their inner workings can be customized based on type traits defined for the client types. All client definitions are in other namespaces.

For the simplest example possible, a library function would basically have to look like this (note that all the code snippets are just wishful thinking, nothing compiles):

namespace lib
{
    template<typename T> void f()
    {
        std::cout << traits_for<T>::str() << '\n'; //Use the traits in some way.
    }
}

Client code would look like this:

namespace client
{
    struct A { };
    template<> std::string traits_for<A>::str() { return "trait value"; }
}

And then someone, somewhere could call

lib::f<client::A>();

and everything would magically work (the specialization of lib::f() would find the traits explicit specialization in the namespace where the template argument for T is declared, just like ADL does for functions and their arguments). The goal is to make it as easy as possible for client code to define those traits (there could be several) for each client class (there could be lots of those).

Let's see what we could do to make this work. The obvious thing is to define a traits class primary template in lib, and then explicitly specialize it for client types. But then clients can't define those explicit specializations in their own namespace; they have to exit it, at least up to the global namespace, define the explicit specialization, then re-enter the client namespace, which, for maximum fun, could be nested. I'd like to keep the trait definitions close to each client class definition, so this namespace juggling would have to be done near each class definition. Suddenly, a one-liner in client code has turned into a messy several-liner; not good.

To allow the traits to be defined in the client namespace, we could turn the traits class into a traits function, that could be called from lib like this:

traits_for(T())

but now we're creating an object of class T just to make ADL kick in. Such objects could be expensive to construct (or even impossible in some circumstances), so this isn't good either. We have to keep working with types only, not their instances.

Giving up and defining the traits as members of the client classes is not an option either.

Some plumbing required to make this work would be acceptable, as long as it doesn't complicate the definitions for each class and trait in the client namespace (write some code once, but not for every definition).

I've found a solution that satisfies these stringent requirements, and I'll write it up in an answer, but I'd like to find out what people think about this: alternatives, critique of my solution, comments about how all of this is either bleeding obvious or completely useless in practice, the works...

like image 391
bogdan Avatar asked Aug 10 '26 21:08

bogdan


1 Answers

To find a declaration based on some argument, ADL looks like the most promising direction. So, we'll have to use something like

template<typename T> ??? traits_helper(T);

But we can't create objects of type T, so this function should only appear as an unevaluated operand; decltype springs to mind. Ideally, we shouldn't even assume anything about T's constructors, so std::declval could also be useful:

decltype(traits_helper(std::declval<T>()))

What could this do? Well, it could return the actual traits type if the helper would be declared like this:

template<typename T> traits_for<T> traits_helper(T);

We've just found a class template specialization in another namespace, based on the declaration of its argument.

EDIT: Based on a comment from Yakk, traits_helper() should take a T&&, to allow it to work if T's move constructor is not available (the function may not actually be called, but the semantic constraints required for calling it must be met). This is reflected in the complete sample below.

All put together in a standalone example, it looks like this:

#include <iostream>
#include <string>
#include <utility>

namespace lib
{
    //Make the syntax nicer for library code.
    template<typename T> using traits_for = decltype(traits_helper(std::declval<T>()));

    template<typename T> void f()
    {
        std::cout << traits_for<T>::str() << '\n';
    }
}

namespace client_1
{
    //The following two lines are needed only once in every client namespace.
    template<typename> struct traits_for { static std::string str(); };
    template<typename T> traits_for<T> traits_helper(T&&); //No definition needed.

    struct A { };
    template<> std::string traits_for<A>::str() { return "trait value for client_1::A"; }

    struct B { };
    template<> std::string traits_for<B>::str() { return "trait value for client_1::B"; }
}

namespace client_2
{
    //The following two lines are needed only once in every client namespace.
    template<typename> struct traits_for { static std::string str(); };
    template<typename T> traits_for<T> traits_helper(T&&); //No definition needed.

    struct A { };
    template<> std::string traits_for<A>::str() { return "trait value for client_2::A"; }
}

int main()
{
    lib::f<client_1::A>(); //Prints 'trait value for client_1::A'.
    lib::f<client_1::B>(); //Prints 'trait value for client_1::B'.
    lib::f<client_2::A>(); //Prints 'trait value for client_2::A'.
}

Note that no objects of type T or traits_for<T> are created; the traits_helper specialization is never called - only its declaration is used.

like image 87
bogdan Avatar answered Aug 13 '26 11:08

bogdan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!