65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (3投票s)

2016年1月29日

CPOL

7分钟阅读

viewsIcon

14386

downloadIcon

145

intros_ptree:一个允许您从XML文件(或json或ini文件)自动填充结构或类的库,反之亦然。

引言

这是两部分文章系列的第二部分。

  1. 在上篇文章(链接)中,我们已经了解了如何使用intros_ptree库从XML文件(或json或ini文件)自动填充结构(或类)。
  2. 在本文中,我们将探讨该库是如何实现这一功能的。

您可以在本文的示例中找到位于utils_examples/utils_examples.cpp文件中的示例。

本文附带intros_ptree库,以及如何使用它的示例,

概述

引用

问题:你如何吃掉一头大象?

答案:一次吃一口。

在我们开始解决问题之前,我们需要开发一些辅助工具。借助这些工具,我们可以

  1. 测试表达式的有效性
  2. 为条件获取合适的标签
  3. 设计类型内省机制

然后,我们将看到如何使用我们新开发的工具轻松实现目标。让我们开始吧。

解决方案

测试表达式的有效性

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 有一些优势。例如:

  1. SFINAE 对人类来说很棘手。目前,SFINAE 对编译器来说也很棘手,正如我们从之前的例子中看到的。
  2. 我们无法使用 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。如果是,则返回该类型。或者更准确地说,它继承自该类型。因此,通过这个,我们可以得到第一个::valuetrue的类型。

我们想要的功能非常接近于此。唯一的区别是,我们不想返回满足条件的类型,而是想要与之配对的类型。

恰巧,通过从 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_NAMEADD_INTROS_ITEM_USER_NAME)。

回到我们的原始问题

现在我们已经实现了类型内省、表达式测试和get_tag,现在我们准备回到我们的原始问题。

目标

对于任何具有内省支持的结构或类:

  1. 提供一种从类型填充 ptree 的方法。
  2. 提供一种从 ptree 填充类型的方法。

现在这项任务变得微不足道了。我们只需要:

  1. 获取结构的类型内省信息。
  2. 遍历结构,并为每个项:
    1. 获取项的名称。
    2. 将名称-值对写入树(针对目标 1)。
    3. 从树中获取该名称的值并写入结构(针对目标 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填充类型。

结论

我在开发这个库的过程中非常愉快。希望您在使用它时也能感到快乐。有什么想说的,请在评论区告诉我。

© . All rights reserved.