模拟依赖于参数的模板参数查找

时间:2015-01-05 17:45:18

标签: c++ templates namespaces argument-dependent-lookup

我最近在写一些类似于库的代码时遇到了这个问题,我认为讨论它也可能对其他人有所帮助。

假设我有一个库,其中包含一些在命名空间中定义的函数模板。函数模板适用于客户端代码提供的类型,其内部工作可以根据为客户端类型定义的类型特征进行自定义。所有客户端定义都在其他名称空间中。

对于最简单的例子,库函数基本上必须看起来像这样(请注意,所有代码片段只是一厢情愿,没有编译):

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

客户端代码如下所示:

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

然后有人可以打电话给某人

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

并且一切都会神奇地起作用(lib::f()的特化会在命名空间中找到特征显式特化,其中声明了T的模板参数,就像ADL对函数及其参数一样)。目标是让客户端代码尽可能简单地为每个客户端类定义这些特性(可能有几个)(可能有很多)。

让我们看看我们可以做些什么来完成这项工作。显而易见的是在lib中定义特征类主模板,然后明确地将其专门用于客户端类型。但是客户端无法在自己的命名空间中定义那些显式特化;他们必须退出它,至少到全局命名空间,定义显式特化,然后重新输入client命名空间,为了最大的乐趣,可以嵌套。我想将特征定义保持在每个客户端类定义附近,因此必须在每个类定义附近完成这个命名空间杂耍。突然之间,客户代码中的单行代码变成了一个混乱的多线程;不好。

为了允许在client命名空间中定义特征,我们可以将特征类转换为特征函数,可以从lib调用,如下所示:

traits_for(T())

但是现在我们正在创建一个类T的对象,只是为了让ADL启动。这样的对象构造起来可能很昂贵(在某些情况下甚至是不可能的),所以这也不好。我们必须继续使用类型,而不是它们的实例。

放弃和定义特征作为客户类的成员也不是一种选择。

只要它不会使client命名空间中的每个类和特征的定义复杂化(编写一些代码,但不是每个定义),那么完成此工作所需的一些管道是可以接受的。

我找到了满足这些严格要求的解决方案,我会在答案中写出来,但我想知道人们对此的看法:替代方案,对我的解决方案的批评,对如何评论所有这些都是在实践中明显或完全无用的出血,作品......

3 个答案:

答案 0 :(得分:1)

要根据某些参数找到声明,ADL看起来是最有希望的方向。所以,我们必须使用像

这样的东西
template<typename T> ??? traits_helper(T);

但是我们不能创建T类型的对象,所以这个函数应该只显示为未评估的操作数;我想起了decltype。理想情况下,我们甚至不应假设T的构造函数,因此std::declval也可能有用:

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

这可以做什么?好吧,如果帮助器声明如下,它可以返回实际的traits类型:

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

我们刚刚在另一个命名空间中找到了一个类模板特化,基于其参数的声明。

编辑:根据Yakk的评论,traits_helper()应该使用T&&,以便在T的移动构造函数不可用时允许它工作(该函数可能实际上不可用)被调用,但必须满足调用它所需的语义约束。这反映在下面的完整示例中。

所有这些都放在一个独立的例子中,它看起来像这样:

#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'.
}

请注意,不会创建Ttraits_for<T>类型的对象;从不调用traits_helper专门化 - 仅使用其声明。

答案 1 :(得分:0)

仅仅要求客户将其专业化投入正确的命名空间有什么不对?如果他们想要使用自己的,他们可以:

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
    { };
}

如果您不希望他们写出所有内容,甚至可以为您的用户提供一个宏:

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

所以上面的内容可以成为

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

答案 2 :(得分:0)

ADL太棒了。保持简单:

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>{};
}

现在只需添加类型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*)

}

我们完成了。定义traits_func可以转换foo*的指针就足以注入特征。

如果你没有写出这样的重载,我们会得到一个空的traits,这是SFINAE友好的。

您可以在重载中返回lib::no_traits以明确关闭支持,或者只是编写与类型匹配的重载。