【问题标题】:Simulating argument-dependent lookup for template arguments模拟模板参数的参数相关查找
【发布时间】:2015-01-05 17:45:18
【问题描述】:

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

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

对于最简单的示例,库函数基本上必须如下所示(注意所有代码 sn-ps 只是一厢情愿,没有任何编译):

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 中定义一个traits 类主模板,然后显式地将其专门用于客户端类型。但是客户不能在他们自己的命名空间中定义那些显式的特化;他们必须退出它,至少到全局命名空间,定义明确的特化,然后重新进入 client 命名空间,为了获得最大的乐趣,可以嵌套。我想将特征定义保持在每个客户端类定义附近,因此必须在每个类定义附近完成这个命名空间杂耍。突然,客户端代码中的单行代码变成了杂乱无章的多行代码;不好。

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

traits_for(T())

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

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

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

我找到了满足这些严格要求的解决方案,我会将其写在答案中,但我想了解人们对此的看法:替代方案、对我的解决方案的批评、cmets 关于如何所有这一切要么明显流血,要么在实践中完全没用,工作......

【问题讨论】:

  • @sbabbi 不错的发现。相同的技术应用于略有不同且更复杂的环境中。我能说什么......伟大的思想都一样:-)
  • 为什么不简单地nest the traits within the client class?如果您的目标只是从类型映射到类型,那么成员类型(或 typedef)就非常简单。
  • @Casey 确实,我没有给出任何不想要的理由,问题只是说“这不是一个选择”,但我认为这是一个合理的要求。通过单独的特征,lib 代码也可以以统一的方式为内置类型定制。此外,客户端类可以在应用程序的多个不同上下文中使用;与lib 的接口只是其中之一。保持核心类定义尽可能简单并且不将它们绑定到特定库代码的要求(如果可能的话)是使用单独特征类的另一个原因。

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


【解决方案1】:

要找到基于某个参数的声明,ADL 看起来是最有希望的方向。所以,我们必须使用类似的东西

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

但是我们不能创建T类型的对象,所以这个函数应该只作为一个未计算的操作数出现; decltype 浮现在脑海中。理想情况下,我们甚至不应该假设任何关于 T 的构造函数的事情,所以 std::declval 也可能有用:

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

这能做什么?好吧,如果 helper 像这样声明,它可以返回实际的特征类型:

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

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

编辑:根据 Yakk 的评论,traits_helper() 应该采用 T&amp;&amp;,以便在 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&lt;T&gt; 类型的对象; traits_helper 特化永远不会被调用——只使用它的声明。

【讨论】:

  • 为什么不接受T* 并通过(T*)nullptr?当然,没有太大不同,但似乎不那么古怪。
  • 嗯。私人复制ctor?
  • @Yakk 关于私有/已删除复制构造函数的观点非常好;我忽略了这一点。看起来traits_helper() 应该采用T&amp;&amp;。我更喜欢T&amp;&amp;std::declval();对我来说,this 看起来不那么恶心,因为我认为declval 是一种标准的说法,即“不要尝试构建这个”;只是个人喜好。
  • client::traits_forclient::traits_helper 都不需要是模板,you could eliminate some boilerplate
  • @Casey 实际上,这样做是在添加样板。在您的解决方案中,对于client 中的每个类,我们需要定义特定的特征类,声明特征辅助函数重载,然后在必要时定义各个特征。正如评论所说,使用模板,这些行只需要一次。
【解决方案2】:

只要求客户将他们的专业化放在正确的命名空间中有什么问题?如果他们想使用自己的,他们可以:

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)

【讨论】:

  • 正如问题中提到的那样,真的没有什么问题。除了更多的打字、更丑陋的代码、为每个特征类跟踪两个位置以及宏(我可以忍受它们,但如果可能的话我宁愿没有它们)。
【解决方案3】:

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* 转换的指针就足以注入 trait。

如果您未能编写这样的重载,我们会得到一个空的 traits,这是 SFINAE 友好的。

您可以在重载中返回 lib::no_traits 以显式关闭支持,或者干脆不要编写与类型匹配的重载。

【讨论】:

  • 一个充满有价值替代方案的解决方案;谢谢你。从通过decltype 指定的东西派生而不是使用别名值得作为一个选项。关于traits_func() 的定义,我宁愿省略它们:如果由于某种原因,我最终在评估它的上下文中使用了这样的函数,这意味着我将它用于不同于其原始目的的东西;没关系,但我更喜欢在确定这不是问题之前通过编译器错误消息提醒这一点。
  • struct no_traits在这里有什么特殊用途吗?
  • 提供default_traits 是个好主意。但是,实现取决于默认的 traits_func() 是一个模板,而客户端命名空间中的那些是普通函数。这是我不喜欢的部分:这意味着客户端需要声明traits_func(),除了foo_traits,对于每个foo;这是我宁愿避免的一种并发症。如果客户需要默认值,我宁愿允许客户从default_traits 派生。
  • 此外,如果客户端忘记为foo 声明traits_func(),它将被静默映射到default_traits。这可能是一个特性或问题,但我宁愿通过编译器错误使其明确。
  • @bogan 将details::default_traits 替换为上面的no_traits 以使默认设置为空。它不依赖于任何不是非模板的东西。对于名称空间中的traits_funcX* 不匹配的类型X,它确实会产生一些不幸的硬错误。嗯,我可以解决这个问题。并删除默认值,为 SFINe 提供友好的空特征。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-22
  • 1970-01-01
  • 1970-01-01
  • 2015-08-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多