从XML自动填充结构(第2部分,共2部分)






4.75/5 (3投票s)
intros_ptree:一个允许您从XML文件(或json或ini文件)自动填充结构或类的库,反之亦然。
引言
这是两部分文章系列的第二部分。
- 在上篇文章(链接)中,我们已经了解了如何使用
intros_ptree
库从XML文件(或json或ini文件)自动填充结构(或类)。 - 在本文中,我们将探讨该库是如何实现这一功能的。
您可以在本文的示例中找到位于utils_examples/utils_examples.cpp文件中的示例。
本文附带intros_ptree
库,以及如何使用它的示例,
概述
引用问题:你如何吃掉一头大象?
答案:一次吃一口。
在我们开始解决问题之前,我们需要开发一些辅助工具。借助这些工具,我们可以
- 测试表达式的有效性
- 为条件获取合适的标签
- 设计类型内省机制
然后,我们将看到如何使用我们新开发的工具轻松实现目标。让我们开始吧。
解决方案
测试表达式的有效性
SFINAE:如果您不知道这是什么,它会让您大吃一惊。它允许您编写错误的 C++ 代码!!!而编译器会默默地忽略它。(这听起来不对!!!如果编译器忽略它,并且这是标准行为,那么就意味着代码是正确的。)
基本上,我们使用 SFINAE 来选择正确的重载(或特化)。错误的重载将包含一个错误的(无法编译的)表达式。但由于 SFINAE,编译器不会给出编译错误,而是会默默地忽略它。因此有了“替换失败不是错误”这个名字。
让我们来看一些 SFINAE 的实际应用。
假设我们要检查一个类型是否具有成员变量x
。我们可以通过 SFINAE 以如下方式实现:
#include <type_traits>
template<typename... Ts> struct make_void { typedef void type; };
template<typename... Ts> using void_t = typename make_void<Ts...>::type;
template<typename T, typename = void>
struct has_x : std::false_type {};
template<typename T>
struct has_x<T, void_t<decltype(T::x)>> : std::true_type {};
int main()
{
struct A { int x; int y; };
static_assert(!has_x<int>::value, "");
static_assert(has_x<A>::value, "");
}
这是有效的 C++ 代码。但它在 Visual Studio 2015 Update 1 中会失败。更糟糕的是,这种失败是不一致的。它可能工作一段时间然后开始失败。
所以,我们需要一些替代方法来解决这个问题。
引用任何问题都可以通过增加一个间接层来解决。
#include <type_traits>
template<typename... Ts> struct make_void { typedef void type; };
template<typename... Ts> using void_t = typename make_void<Ts...>::type;
template<typename T, typename = void_t<decltype(T::x)>>
std::true_type has_x_impl2();
template<typename T>
decltype(has_x_impl2<T>()) has_x_impl1(void*);
template<typename T>
std::false_type has_x_impl1(...);
template<typename T>
struct has_x : decltype(has_x_impl1<T>(nullptr))
{};
int main()
{
struct A { int x; int y; };
static_assert(!has_x<int>::value, "");
static_assert(has_x<A>::value, "");
}
太棒了!!!我们现在知道,A
有一个成员x
。
只是,写这段代码不太有趣。
所以,这里有一个辅助宏,让我们的生活更轻松。
IMPLEMENT_EXPR_TEST(name, test, ...)
此宏可在utils/is_valid_expression.hpp文件中找到。
借助该宏,我们现在可以像这样检查一个类型是否具有成员x
:
IMPLEMENT_EXPR_TEST(has_x, decltype(T::x), T)
static_assert(has_x<A>::value, "");
不错……这样好多了。
我们来看另一个例子。
假设您想执行以下检查:
static_assert(has_begin<T>::value, "");
其中,has_begin
应该表示类型T
是否具有begin
成员函数。然后,您需要像这样使用IMPLEMENT_EXPR_TEST
:
IMPLEMENT_EXPR_TEST(has_begin, decltype(declval<T&>().begin()), T)
这里,第一个参数是测试的名称。第二个参数是您想要执行的实际表达式测试。第三个参数是您在表达式测试中使用的一个或多个类型。
让我展示另一种执行上述测试的方法。
template<typename T> using has_begin_test = decltype(declval<T&>().begin());
IMPLEMENT_EXPR_TEST_INDIRECT(has_begin, has_begin_test, T)
static_assert(has_begin<T>::value, "");
在这里,我们使用别名模板作为表达式测试。
当您的表达式更复杂时,这可能会很有用。
您可以在utils_test/test_is_valid_expression.cpp文件中找到更多关于表达式测试的示例。
为条件获取合适的标签
标签分派:标签分派是我们用来选择正确重载函数的机制。我们来看一个例子:
Template<typename T>
void f(const T& ob, tag_type_is_container);
Template<typename T>
void f(const T& ob, tag_else);
这里,我们看到函数f
有两个重载。一个我们希望在T
是容器时选择。另一个我们想在所有其他情况下使用。
我们可以使用 SFINAE 来实现这一点。但是标签分派比 SFINAE 有一些优势。例如:
- SFINAE 对人类来说很棘手。目前,SFINAE 对编译器来说也很棘手,正如我们从之前的例子中看到的。
- 我们无法使用 SFINAE 优先选择函数重载。也就是说:
template<typename T>
auto f(const T& ob) -> decltype(cout << ob, void())
{
// choose this if we can stream ob to cout
}
template<typename T>
auto f(const T& ob) -> decltype(ob.begin(), void())
{
// choose this if ob has begin member
}
f(std::string());
上面的代码无法编译。这是因为,对于std::string
,两个f
重载都满足我们的 SFINAE 条件。所以,编译器无法在它们之间选择一个重载。用户也无法指示在这种情况下选择可流式传输的(或具有 begin 成员函数的)那个。
现在,让我们尝试使用标签分派来处理上面的情况。
我们想写的是这个:
// these empty classes is what we are calling tags
// they exist only to help us choose the right overloaded function
class tag_is_streamable{};
class tag_is_container{};
template<typename T>
void f(const T&, tag_is_streamable) // we don't even have to give a variable name for the tag
{}
template<typename T>
void f(const T&, tag_is_container)
{}
std::string s;
f(s, my_tag<T>());
这里,my_tag
是我们想要解决的难题。我们想以一种方式来写它,这样我们就可以将条件与标签配对,并且无论哪个条件首先得到满足,我们都能得到相应的标签。就像这样:
template<typename T>
using my_tag = get_tag<
is_streamable<T>, tag_is_streamable,
is_container<T>, tag_is_container
>;
我们可以这样实现检查:
IMPLEMENT_EXPR_TEST(is_streamable,
decltype(std::declval<std::ostream&>() << std::declval<T&>()), T)
IMPLEMENT_EXPR_TEST(is_container, decltype(std::declval<T&>().begin()), T)
所以,我们已经将检查与标签配对了。但是如何编写get_tag
呢?
std::disjunction
Disjunction 是 C++17 的一个特性。cppreference给出了一个可能的实现如下:
template<class...> struct disjunction : std::false_type { };
template<class B1> struct disjunction<B1> : B1 { };
template<class B1, class... Bn>
struct disjunction<B1, Bn...> :
std::conditional_t<B1::value != false, B1, disjunction<Bn...>> { };
所以,它的作用是,对于每个类型T
,它检查T::value
是否为true
。如果是,则返回该类型。或者更准确地说,它继承自该类型。因此,通过这个,我们可以得到第一个::value
为true
的类型。
我们想要的功能非常接近于此。唯一的区别是,我们不想返回满足条件的类型,而是想要与之配对的类型。
恰巧,通过从 disjunction 中获得灵感,我们可以轻松地实现get_tag
。以下是实现:
template<class...> struct get_tag {};
template<class def_tag> struct get_tag<def_tag> : def_tag {};
template<class cond1, class tag1, class... Bn>
struct get_tag<cond1, tag1, Bn...> :
std::conditional_t<cond1::value != false, tag1, get_tag<Bn...>> {};
太棒了!!!我们现在有了get_tag
的实现。
get_tag
可在utils/util_traits.hpp文件中,位于utils::traits
命名空间下。
您可以在utils_test/test_util_traits.cpp文件中找到更多关于get_tag
的示例。
设计类型内省机制
反射:维基百科关于反射是这样说的:“在计算机科学中,反射是指计算机程序在运行时检查(参见类型内省)和修改其自身结构和行为(特别是值、元数据、属性和函数)的能力。[1]”
在某些语言中,我们可以查询类型的全部成员,但 C++ 不是其中之一。所以,让我们看看我们能为此做些什么。
std::tuple
是一个可以保存不同类型值的类。因此,如果我们能够用struct
的所有成员填充一个std::tuple
,那么我们就可以查询该类型的成员了!!!
以下是我们如何做到这一点。让我们再次看看我们的book struct
:
struct book
{
int id;
string name;
string author;
};
book b;
现在,让我们创建一个包含b
成员的元组:
auto t = tuple<string, int, string>(b.author, b.id, b.name);
voilà。一个包含book
所有成员的元组。
但是修改t
不会反映在b
中。而我们希望如此。
为了实现这一点,我们需要让元组的项成为引用。
auto t = tuple<string&, int&, string&>(b.author, b.id, b.name);
现在,对t
的修改将反映在b
中。太棒了!!!
并且使用 boost fusion 库的for_each
函数,我们甚至可以迭代元组。
下面的 4 个宏泛化了这个概念,以便我们可以轻松地将其用于任何结构:
BEGIN_INTROS_TYPE(type)
BEGIN_INTROS_TYPE_USER_NAME(type, name)
END_INTROS_TYPE(type)
ADD_INTROS_ITEM(x)
ADD_INTROS_ITEM_USER_NAME(x, name)
一旦我们输入:
BEGIN_INTROS_TYPE(book)
ADD_INTROS_ITEM(id)
ADD_INTROS_ITEM(name)
ADD_INTROS_ITEM(author)
END_INTROS_TYPE(book)
然后,会为 book 定义两个如下的函数:
auto get_intros_type(book& val);
auto get_intros_type(const book& val);
然后我们可以这样做:
auto intros_object = get_intros_type(b);
这里intros_object
是以下模板类型:
template<typename... TupleItems>
struct intros_type
{
std::string name; // the type name
std::tuple<TupleItems...> items; // members of the type
};
并且每个TupleItems
都是以下类型:
template<typename T>
struct intros_item
{
std::string name; // name of the item
T& val; // member value as reference... this will be const reference if type is const
};
现在,我们可以编写一个如下的函数:
void intros_example()
{
book b;
// if you think auto is not a very useful addition to C++
// then try cout << typeid(intros_object).name()
// auto just saved us from writing that horrible type
auto intros_object = get_intros_type(b);
cout << "Type name: " << intros_object.name << "\n";
// set values
auto t = intros_object.items;
std::get<0>(t).val = 1234;
cout << "Name of item: "
<< std::get<0>(t).name << "\n";
cout << "Value from b: " << b.id <<
"\tValue from intros: " << std::get<0>(t).val << "\n";
}
这将打印:
Type name: book
Name of item: id
Value from b: 1234 Value from intros: 1234
您可以在utils_test/test_intros_type.cpp文件中找到更多关于类型内省的示例。
注意:还有其他库提供类型内省支持(例如 boost fusion、boost hana)。我提供自己实现类型内省的原因是,我想允许为变量使用不同的名称(BEGIN_INTROS_TYPE_USER_NAME
和ADD_INTROS_ITEM_USER_NAME
)。
回到我们的原始问题
现在我们已经实现了类型内省、表达式测试和get_tag
,现在我们准备回到我们的原始问题。
目标
对于任何具有内省支持的结构或类:
- 提供一种从类型填充 ptree 的方法。
- 提供一种从 ptree 填充类型的方法。
现在这项任务变得微不足道了。我们只需要:
- 获取结构的类型内省信息。
- 遍历结构,并为每个项:
- 获取项的名称。
- 将名称-值对写入树(针对目标 1)。
- 从树中获取该名称的值并写入结构(针对目标 2)。
我们还需要对项进行分类(例如,可流式传输项、容器项等)。使用我们的表达式测试和
get_tag
工具,我们可以轻松完成。我不想因为如何处理每种项类型而让您感到厌烦。您可以查看源代码。
就是这样。现在我们可以编写
template<typename T>
boost::property_tree::ptree make_ptree(const T& in)
函数来获取类型的ptree
。
我们可以编写
template<typename T>
T make_intros_object(const boost::property_tree::ptree& tree)
函数来从ptree
填充类型。
结论
我在开发这个库的过程中非常愉快。希望您在使用它时也能感到快乐。有什么想说的,请在评论区告诉我。