我最近在写一些类似于库的代码时遇到了这个问题,我认为讨论它也可能对其他人有所帮助。
假设我有一个库,其中包含一些在命名空间中定义的函数模板。函数模板适用于客户端代码提供的类型,其内部工作可以根据为客户端类型定义的类型特征进行自定义。所有客户端定义都在其他名称空间中。
对于最简单的例子,库函数基本上必须看起来像这样(请注意,所有代码片段只是一厢情愿,没有编译):
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
命名空间中的每个类和特征的定义复杂化(编写一些代码,但不是每个定义),那么完成此工作所需的一些管道是可以接受的。
我找到了满足这些严格要求的解决方案,我会在答案中写出来,但我想知道人们对此的看法:替代方案,对我的解决方案的批评,对如何评论所有这些都是在实践中明显或完全无用的出血,作品......
答案 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'.
}
请注意,不会创建T
或traits_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
以明确关闭支持,或者只是编写与类型匹配的重载。