Simulate argument-dependent template argument lookups

I ran into this issue while writing some library code lately, and I thought discussing it might help others as well.

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

For the simplest example, it is possible that the library function should look something like this (note that all 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.
    }
}

      

The client code will look like this:

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

      

And then someone could call

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

      

and everything will work magically (specialization lib::f()

will find traits with explicit specialization in the namespace where the template argument is declared for T

, like ADL for functions and their arguments). The goal is to make the client code as simple as possible to define these attributes (there may be several) for each client class (there may be many).

Let's see what we can do to make this work. The obvious is to define a primary template for a feature class in lib

, and then explicitly specialize it for customer types. But then clients cannot define these explicit specializations in their own namespace; they should get out of it at least up to the global namespace, define an explicit specialization, then re-enter the namespace client

, which can be nested for maximum fun. I would like the trait definitions to be close to every client class definition, so this namespace juggling has to be done next to every class definition. All of a sudden, the one-line client code turned into a messy few liners; not good.

To allow the definition of features in the namespace client

, we could turn the feature class into a feature function that could be called from lib

as follows:

traits_for(T())

      

but now we create a class object T

just to do ADL. Such objects can be expensive to create (or even impossible in some cases), so that's not good either. We should continue to work only with types, not instances of them.

Refusal to define and define characteristics as members of client classes is also not.

Some plumbing required to get the job done will be fine as long as it doesn't complicate the definitions for each class and characteristic in the namespace client

(write the code once, but not for each definition).

I found a solution that meets these strict requirements and I'll post it in an answer, but I would like to know what people think about it: alternatives, criticism of my solution, comments on how it all either bleeds obvious or completely useless in practice , work ...

+3


source to share


3 answers


To find an ad based on some argument, ADL looks like the most promising direction. So, we will need to use something like

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

      

But we cannot create objects of type T

, so this function should only appear as an unvalued operand; decltype

Comes to mind. Ideally, we shouldn't even think about constructors T

, so std::declval

it might also be helpful:

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

      

What can this do? Well, it can return the type of the actual traits if the helper is declared like this:

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

      



We just found a specialization of a class template in a different namespace based on the declaration of its argument.

EDIT: Based on a comment from Yakk traits_helper()

should be taken T&&

to allow it to work if the move constructor is T

not available (the function cannot actually be called, but the required semantic constraints to call this must be met). This is reflected in the full sample below.

All collected in a separate example 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 objects of type T

or are not created traits_for<T>

; the specialization is traits_helper

never called - only its declaration is used.

+1


source


What's wrong with simply requiring clients to drop their specialization in the correct namespace? If they want to use their own, they can:

namespace client
{
    struct A { };

    struct traits_for_A {
        static std::string str() { return "trait value"; }
    };

}

namespace lib 
{
    template <>
    struct traits_for<client::A>
    : client::traits_for_A
    { };
}

      

Might even give your users a macro if you don't want them to record it all:



#define PROVIDE_TRAITS_FOR(cls, traits) \
    namespace lib { \
        template <> struct traits_for<cls> : traits { }; \
    }

      

So the above can be

PROVIDE_TRAITS_FOR(client::A, client::traits_for_A)

      

0


source


ADL is awesome. Keep it simple:

namespace lib {
  // helpers for client code:
  template<class T>
  struct default_traits{
    using some_type=void;
  };
  struct no_traits{};
  namespace details {
    template<class T,class=void>
    struct traits:lib::no_traits{};
    template<class T>
    struct traits<T,decltype(void(
      traits_func((T*)0)
    ))>:decltype(
      traits_func((T*)0)
    ){};
  }
  template<class T>
  struct traits:details::traits<T>{};
}

      

Now just add the namespace Foo

:

namespace bob{
  // use custom traits impl:
  struct foo{};
  struct foo_traits{
    using some_type=int;
  };
  foo_traits traits_func(foo const*);

  // use default traits impl:
  struct bar {};
  lib::default_traits<bar> traits_func(bar const*);

  // use SFINAE test for any type `T`:
  struct baz {};
  template<class T>
  std::enable_if_t<
    std::is_base_of<T,baz>{},
    lib::default_traits<T>
  >
  traits_func(T const*)

}

      

and we're done. A definition traits_func

that accepts a pointer to be converted from foo*

is sufficient to insert a tag.

If you can't write an overload like this, we'll traits

end up with an empty one , which is SFINAE friendly.

You can revert lib::no_traits

to overloading to explicitly disable support, or simply not write an overload that matches the type.

0


source







All Articles