内省
我们将在这里讨论静态内省,静态内省是程序在编译时检查对象类型的能力。 换句话说,它是一个在编译时与类型交互的编程接口。 例如,你曾经想检查一些未知类型是否有一个名为foo
的成员? 或者在某些时候你需要迭代结构的成员?
struct Person {
std::string name;
int age;
};
Person john{"John", 30};
for (auto& member : john)
std::cout << member.name << ": " << member.value << std::endl;
// name: John
// age: 30
如果你在你的生活中写了一些模板,你遇到的第一个问题是检查一个成员的机会很高。 此外,任何人试图实现对象序列化,甚至只是格式化输出就会产生第二个问题。在大多数动态语言如Python
,Ruby
或JavaScript
中,这些问题都是能完全解决的,程序员每天都使用内省来简化很多任务。 然而,作为一个C++
程序员,我们没有语言支持这些东西,这使得完成这几个任务比他们应该更困难。 虽然处理这个问题可能需要等待语言支持,但使用Hana
可以很容易获得一些常见的内省模式。
表达式有效性检查
给定一个未知类型的对象,有时需要检查这个对象是否有一个具有某个名字的成员(或成员函数)。 这可以用于执行复杂的重载风格。 例如,考虑对支持它的对象调用toString
方法的问题,但为不支持它的对象提供另一个默认实现:
template <typename T>
std::string optionalToString(T const& obj) {
if (obj.toString() is a valid expression)
return obj.toString();
else
return "toString not defined";
}
注意: 虽然这种技术的大多数用例将通过在未来修订标准中的concepts lite概念来解决,但是仍然存在这样的情况,其中快速、脏检查比创建完全概括的概念更方便。
我们如何以通用方式实现对obj.toString()
的有效性的检查(因此它可以在其他函数中重用)? 通常,我们会想到写一些基于SFINAE
的检查:
template <typename T, typename = void>
struct has_toString
: std::false_type
{ };
template <typename T>
struct has_toString<T, decltype((void)std::declval<T>().toString())>
: std::true_type
{ };
代码能很好地工作,但代码想表达的意图不是很直观,大多数没有深刻的模板元编程知识的人会认为这是黑魔法。 然后,我们可以实现optionalToString
:
template <typename T>
std::string optionalToString(T const& obj) {
if (has_toString<T>::value)
return obj.toString();
else
return "toString not defined";
}
注意: 当然,这个实现不会真正工作,因为
if
语句的两个分支都将被编译。 如果obj
没有toString
方法,if
分支的编译将失败。 我们将在稍后解决这个问题。
代替上面的SFINAE
技巧,Hana
提供了一个is_valid
函数,可以与C++14通用lambdas组合获得一个更干净的实现:
auto has_toString = hana::is_valid([](auto&& obj) -> decltype(obj.toString()) { });
这里我们有一个函数对象has_toString
返回给定的表达式是否对我们传递给它的参数有效。 结果作为IntegralConstant
返回,因此constexpr-ness
在这里不是一个问题,因为函数的结果表示为一个类型。 现在,除了代码更少(这是一个单行!),意图也更清晰外,还有其他好处是,has_toString
可以传递到更高阶的算法,它也可以在函数范围定义,因此没有必要污染具有实现细节的命名空间范围。 下面是我们将如何编写的optionalToString
:
template <typename T>
std::string optionalToString(T const& obj) {
if (has_toString(obj))
return obj.toString();
else
return "toString not defined";
}
更干净,对吧? 然而,正如我们前面所说的,这个实现不会真正工作,因为if
的两个分支总是必须被编译,不管obj
是否有toString
方法。 有几个可能的选项,但最古典的是使用std::enable_if
:
template <typename T>
auto optionalToString(T const& obj)
-> std::enable_if_t<decltype(has_toString(obj))::value, std::string>
{ return obj.toString(); }
template <typename T>
auto optionalToString(T const& obj)
-> std::enable_if_t<decltype(!has_toString(obj))::value, std::string>
{ return "toString not defined"; }
注意: 我们使用这样一个事实,
has_toString
返回一个IntegralConstant
,因而decltype(...)::value
是一个常量表达式。 出于某种原因,has_toString(obj)
不被认为是一个常量表达式,即使我认为它应该是,因为我们从未读过obj
(参见高级constexpr)。
虽然这个实现是完全有效的,但它仍然相当繁琐,因为它需要编写两个不同的函数,并通过使用std::enable_if
显式地绕过了SFINAE
的圈子。 然而,你可能还记得编译时分支那一节,Hana
提供了一个if_
函数,可以用来模拟static_if
的功能。 这里我们用hana::if_
来编写optionalToString
:
template <typename T>
std::string optionalToString(T const& obj) {
return hana::if_(has_toString(obj),
[](auto& x) { return x.toString(); },
[](auto& x) { return "toString not defined"; }
)(obj);
}
前面的示例仅涉及检查是否存在某个非静态成员函数的特定情况。 然而,is_valid
可以用于检测几乎任何种类的表达式的有效性。 以下列出了有效性检查的常见用例以及如何使用is_valid
来实现它们。
非静态成员
我们要看的第一个惯用法是检查非静态成员的存在。 我们可以使用与上一个示例类似的方式:
auto has_member = hana::is_valid([](auto&& x) -> decltype((void)x.member) { });
struct Foo { int member[4]; };
struct Bar { };
BOOST_HANA_CONSTANT_CHECK(has_member(Foo{}));
BOOST_HANA_CONSTANT_CHECK(!has_member(Bar{}));
注意我们为何将x.member
的结果转换为void
? 这是为了确保我们的检测也适用于不能从函数返回的类型,如数组类型。 此外,重要的是使用通用引用作为我们的lambda
的参数,否则将需要x
是复制构造的,这不是我们试图检查的。 这种方法很简单,当对象可用时最方便。 然而,当检查器旨在不使用对象时,以下替代实现可以更好地适合:
auto has_member = hana::is_valid([](auto t) -> decltype(
(void)hana::traits::declval(t).member
) { });
struct Foo { int member[4]; };
struct Bar { };
BOOST_HANA_CONSTANT_CHECK(has_member(hana::type_c<Foo>));
BOOST_HANA_CONSTANT_CHECK(!has_member(hana::type_c<Bar>));
这个有效性检查器不同于我们之前看到的,因为通用的lambda
不再期望一个通常的对象了; 它现在期待一个类型(它是一个对象,但仍然代表一个类型)。 然后,我们使用来自<boost/hana/traits.hpp>
头文件的hana::traits::declval
提升的元函数来创建由t
表示的类型的右值,然后我们可以使用它来检查非静态成员。 最后,不是将实际对象传递给has_member
(像Foo{}
或Bar{}
),我们现在传递一个type_c<...>
。 这个实现是没有对象时的理想选择。
静态成员
检查静态成员是很容易的,并且Hana
提供完整性支持:
auto has_member = hana::is_valid([](auto t) -> decltype(
(void)decltype(t)::type::member
) { });
struct Foo { static int member[4]; };
struct Bar { };
BOOST_HANA_CONSTANT_CHECK(has_member(hana::type_c<Foo>));
BOOST_HANA_CONSTANT_CHECK(!has_member(hana::type_c<Bar>));
再次,我们期望一个类型被传递给检查器。 在通用lambda
中,我们使用decltype(t)::type
来获取由t
对象表示的实际C++
类型,如类型计算节中所述。 然后,我们获取该类型中的静态成员并将其转换为void
,这与非静态成员的原因相同。
嵌套类型名
检查嵌套类型名称并不难,但会稍微复杂一点:
auto has_member = hana::is_valid([](auto t) -> hana::type<
typename decltype(t)::type::member
//^^^^^^^^ needed because of the dependent context
> { });
struct Foo { struct member; /* not defined! */ };
struct Bar { };
BOOST_HANA_CONSTANT_CHECK(has_member(hana::type_c<Foo>));
BOOST_HANA_CONSTANT_CHECK(!has_member(hana::type_c<Bar>));
可能想知道为什么我们使用 ->hana::type<typename-expression>
而不是简单的 ->typename-expression
。 同样,原因是我们要支持不能从函数返回的类型,如数组类型或不完全类型。
嵌套模板
检查嵌套模板名称类似于检查嵌套类型名称,除了在通用lambda
中使用template_<...>
变量模板而不是type<...>
:
auto has_member = hana::is_valid([](auto t) -> decltype(hana::template_<
decltype(t)::type::template member
// ^^^^^^^^ needed because of the dependent context
>) { });
struct Foo { template <typename ...> struct member; };
struct Bar { };
BOOST_HANA_CONSTANT_CHECK(has_member(hana::type_c<Foo>));
BOOST_HANA_CONSTANT_CHECK(!has_member(hana::type_c<Bar>));
SFINAE控制
只有表达式形式良好时才做某事是C++
中非常常见的模式。 实际上,optionalToString
函数只是以下模式的一个实例,这是一般化的形式:
template <typename T>
auto f(T x) {
if (some expression involving x is well-formed)
return something involving x;
else
return something else;
}
为了封装这个模式,Hana
提供了sfinae
函数,它允许执行一个表达式,但是只有当它是良好的形式:
auto maybe_add = hana::sfinae([](auto x, auto y) -> decltype(x + y) {
return x + y;
});
maybe_add(1, 2); // hana::just(3)
std::vector<int> v;
maybe_add(v, "foobar"); // hana::nothing
这里,我们创建一个maybe_add
函数,它只是一个用Hana
的sfinae
函数包装的通用lambda
。 maybe_add
是一个函数,它接受两个输入,返回juse
包装的普通lambda
的结果,如果调用是良好的,just(...)
返回一个类型的容器,称为hana::optional
,它本质上是一个编译时std::optional
。 总而言之,maybe_add
等同于以下函数返回一个std::optional
,除了检查是在编译时完成的:
auto maybe_add = [](auto x, auto y) {
if (x + y is well formed)
return std::optional<decltype(x + y)>{x + y};
else
return std::optional<???>{};
};
事实证明,我们可以利用sfinae
和optional
来实现optionalToString
函数,如下所示:
template <typename T>
std::string optionalToString(T const& obj) {
auto maybe_toString = hana::sfinae([](auto&& x) -> decltype(x.toString()) {
return x.toString();
});
return maybe_toString(obj).value_or("toString not defined");
}
首先,我们使用sfinae
函数包装toString
。 因此,maybe_toString
是一个函数,如果形式良好,则返回(x.toString())
,否则不返回。 其次,我们使用.value_or()
函数从容器中提取可选值。 如果可选值为空,.value_or()
返回给定的默认值; 否则,返回just(x.toString())
。 这种将SFINAE
看作可能失败的计算的特殊情况的方式是非常干净和强大的,特别是因为sfinae'd
函数可以通过hana::optional
Monad
组合,祥情参见参考文档。
内省用户定义类型
你曾经想要遍历用户定义类型的成员吗? 本节的目的是向您展示如何使用Hana
轻松地做到这一点。 为了允许使用用户定义的类型,Hana
定义了Struct
概念。 一旦用户定义的类型是该概念的模型,可以遍历该类型的对象的成员并查询其他有用的信息。 要将用户定义的类型转换为Struct
,可以使用几个选项。 首先,您可以使用BOOST_HANA_DEFINE_STRUCT
宏定义用户定义类型的成员:
struct Person {
BOOST_HANA_DEFINE_STRUCT(Person,
(std::string, name),
(int, age)
);
};
此宏使用给定的类型定义两个成员(名称和年龄)。 然后,它在Person::hana
嵌套结构中定义了一些样板,这是使Person
成为Struct
概念的模型所必需的。 没有定义构造函数(因此保留POD
属性),成员的定义顺序与它们在这里出现的顺序相同,宏可以与模板结构一起使用,也可以在任何范围使用。 另请注意,您可以在使用宏之前或之后向Person
类型中添加更多成员。 但是,只有在使用宏定义的成员在自动检查Person
类型时才会被选中。 足够简单吧? 现在,可以通过编程方式访问Person
:
Person john{"John", 30};
hana::for_each(john, [](auto pair) {
std::cout << hana::to<char const*>(hana::first(pair)) << ": "
<< hana::second(pair) << std::endl;
});
// name: John
// age: 30
完成对结构体的迭代,好像结构体是一对对的序列,其中pair
中的第一个元素是与成员相关的键,第二个元素是成员本身。 当通过BOOST_HANA_DEFINE_STRUCT
宏定义一个Struct
时,与任何成员关联的键是一个编译时hana::string
,表示该成员的名称。 这就是为什么与for_each
一起使用的函数使用单个参数pair
,然后使用first
和second
函数来访问pair
的子部分。 另外,注意如何对成员的名称使用<char const *>
函数的? 这会将编译时字符串转换为constexpr char const *
,所以它可以couted
。 因为总是使用first
和second
来获取对的子部分可能很烦人,我们还可以使用fuse
函数来包装我们的lambda
,并使它成为二个参数的lambda
:
hana::for_each(john, hana::fuse([](auto name, auto member) {
std::cout << hana::to<char const*>(name) << ": " << member << std::endl;
}));
现在,它看起来更简洁。 正如我们刚才提到的,结构体被看作是一种用于迭代目的的pair
序列。 实际上,一个Struct
甚至可以像关联数据结构一样被搜索,其键是成员的名字,其值是成员本身:
std::string name = hana::at_key(john, "name"_s);
BOOST_HANA_RUNTIME_CHECK(name == "John");
int age = hana::at_key(john, "age"_s);
BOOST_HANA_RUNTIME_CHECK(age == 30);
注意:
_s
用户定义的文本创建一个编译时hana::string
。 它位于boost::hana::literals
命名空间中。 请注意,它不是标准的一部分,但受Clang
和GCC
支持。 如果要保持100%
的标准,可以使用BOOST_HANA_STRING
宏。
Struct
和hana::map
之间的主要区别在于hana::map
可以修改映射(可以添加和删除键),而Struct
是不可变的。 但是,您可以轻松地将一个Struct
转换为与<map_tag>
关联的hana::map
,然后您可以以更灵活的方式操作它。
auto map = hana::insert(hana::to<hana::map_tag>(john), hana::make_pair("last name"_s, "Doe"s));
std::string name = map["name"_s];
BOOST_HANA_RUNTIME_CHECK(name == "John");
std::string last_name = map["last name"_s];
BOOST_HANA_RUNTIME_CHECK(last_name == "Doe");
int age = map["age"_s];
BOOST_HANA_RUNTIME_CHECK(age == 30);
使用BOOST_HANA_DEFINE_STRUCT
宏来修改结构很方便,但有时候不能修改需要修改的类型。 在这些情况下,BOOST_HANA_ADAPT_STRUCT
宏可用于以自组织方式调整结构:
namespace not_my_namespace {
struct Person {
std::string name;
int age;
};
}
BOOST_HANA_ADAPT_STRUCT(not_my_namespace::Person, name, age);
注意: 必须在全局范围使用
BOOST_HANA_ADAPT_STRUCT
宏。
该效果与BOOST_HANA_DEFINE_STRUCT
宏完全相同,除非您不需要修改要修改的类型,这有时是有用的。 最后,还可以使用BOOST_HANA_ADAPT_ADT
宏定义自定义访问器:
namespace also_not_my_namespace {
struct Person {
std::string get_name();
int get_age();
};
}
BOOST_HANA_ADAPT_ADT(also_not_my_namespace::Person,
(name, [](auto const& p) { return p.get_name(); }),
(age, [](auto const& p) { return p.get_age(); })
);
这样,用于访问Struct
的成员的名称将是指定的名称,并且在检索该成员时,将在Struct
上调用相关的函数。 在我们继续使用这些内省功能的一个具体例子之前,还应该提到的是,结构可以适应而不使用宏。 这个用于定义Structs
的高级接口可以用于例如指定不是编译时字符串的键。 高级接口在Struct
概念的文档中有描述。
示例:生成JSON
现在让我们继续使用我们刚刚提供的用于以JSON
格式打印自定义对象的内省功能的具体示例。 我们的最终目标是拥有像这样的东西:
struct Car {
BOOST_HANA_DEFINE_STRUCT(Car,
(std::string, brand),
(std::string, model)
);
};
struct Person {
BOOST_HANA_DEFINE_STRUCT(Person,
(std::string, name),
(std::string, last_name),
(int, age)
);
};
Car bmw{"BMW", "Z3"}, audi{"Audi", "A4"};
Person john{"John", "Doe", 30};
auto tuple = hana::make_tuple(john, audi, bmw);
std::cout << to_json(tuple) << std::endl;
格式化JSON输出,应该看起来像:
1 [
2 {
3 "name": "John",
4 "last_name": "Doe",
5 "age": 30
6 },
7 {
8 "brand": "Audi",
9 "model": "A4"
10 },
11 {
12 "brand": "BMW",
13 "model": "Z3"
14 }
15 ]
首先,让我们定义一些效率函数,使字符串操作更容易:
template <typename Xs>
std::string join(Xs&& xs, std::string sep) {
return hana::fold(hana::intersperse(std::forward<Xs>(xs), sep), "", hana::_ + hana::_);
}
std::string quote(std::string s) { return "\"" + s + "\""; }
template <typename T>
auto to_json(T const& x) -> decltype(std::to_string(x)) {
return std::to_string(x);
}
std::string to_json(char c) { return quote({c}); }
std::string to_json(std::string s) { return quote(s); }
quote
和to_json
重载是很自然的。 然而,join
函数可能需要一点解释。 基本上,散布函数采用序列和分隔符,并且在原始序列的每对元素之间返回具有分隔符的新序列。 换句话说,我们采用形式[x1,...,xn]
的序列,并将其转换为形式[x1,sep,x2,sep,...,sep,xn]
的序列。 最后,我们使用_ + _
函数对象折叠结果序列,这等价于std::plus<>{}
。 因为我们的序列包含std::strings
(我们假设它可行),这将具有将序列的所有字符串连接成一个大字符串的效果。 现在,让我们定义如何输出一个序列:
template <typename Xs>
std::enable_if_t<hana::Sequence<Xs>::value,
std::string> to_json(Xs const& xs) {
auto json = hana::transform(xs, [](auto const& x) {
return to_json(x);
});
return "[" + join(std::move(json), ", ") + "]";
}
首先,我们使用transform
算法将我们的对象序列转换为JSON
格式的std::string
序列。 然后,我们用逗号连接该序列,并用[]
将其括起来表示JSON
符号中的序列。 足够简单吧? 现在让我们来看看如何输出用户定义的类型:
template <typename T>
std::enable_if_t<hana::Struct<T>::value,
std::string> to_json(T const& x) {
auto json = hana::transform(hana::keys(x), [&](auto name) {
auto const& member = hana::at_key(x, name);
return quote(hana::to<char const*>(name)) + " : " + to_json(member);
});
return "{" + join(std::move(json), ", ") + "}";
}
这里,我们使用keys
方法来检索包含用户定义类型的成员的名称的元组。 然后,我们将该序列转换为“name”序列:成员字符串,然后我们join
并用{}
括起来,这用于表示JSON
符号中的对象。 收工!