我有一系列数据结构,应该使用boost :: serialization从一层传递到另一层。 例如
struct DataType1
{
std::string field1;
std::string field2;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & field1;
ar & field2;
}
};
我想在此编写单元测试,以确保我没有遗漏某些字段(有很多结构和字段)。
问题是,如果我在结构中添加新字段(我肯定会这样做)而忘记更新单元测试,则单元测试不会覆盖此字段。
我的问题是:如何检测该结构(或类)是否已更改。 我的想法是使用static_assert(sizeof(DataType1)== HARD_CODED_VALUE)但它在不同编译器,平台(x64,x86)和配置(发布,调试)中的结构大小不同。
任何好主意如何处理这个?
答案 0 :(得分:3)
问题是,如果我在结构中添加新字段(我肯定会这样做)而忘记更新单元测试,则单元测试不会覆盖此字段。
我的问题是:如何检测结构(或类)是否已更改。
我的想法是使用static_assert(sizeof(DataType1)== HARD_CODED_VALUE)[...]
这不是便携式解决方案(正如您自己所说)。
任何好主意如何处理这个?
是的:您可以从更新测试开始吗?
也就是说,不要决定结构中的内容,然后添加它,然后更新测试(如果你不忘记)。
相反,更新测试以检查新的序列化数据,然后确保更新的测试失败,然后更新代码以便测试通过。
这种方法(首先是写入/更新单元测试)已经(部分)创建,以解决这个问题。
测试优先方法还有其他优点:
它巧妙地避免了YAGNI
它最大限度地减少了过早优化
它会自然地发展,以跟踪您的应用程序/实现的功能完整性。
答案 1 :(得分:2)
在类定义中添加注释,以提醒您在添加成员时必须调整序列化程序。计算机可以为您做什么是有限的 - 这就是代码审查很重要的原因。让任何补丁都由其他程序员审核,拥有一套严格的测试用例并希望能够做到最好。
我相信你可以,例如编写一个clang插件,它将确保一个特定的方法引用一个结构的每个成员,但是你真的需要这个并且你能投入时间吗?
也就是说,你可以尝试将尽可能多的工作卸载到计算机上。即使是static_assert
技巧也是一个很好的技巧。如果你使用一组#ifdef
来保护它,对于一个特定的ABI和你经常建造的架构,它可能会做得很好。
答案 2 :(得分:1)
如下:
// DataType1_members.h
FIELD_DEF(std::string, field1);
FIELD_DEF(std::string, field2);
// DataType1.h
struct DataType1
{
#define FIELD_DEF(type, name) type name
#include "DataType1_members.h"
#undef FIELD_DEF
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
#define FIELD_DEF(type, name) ar & name
#include "DataType1_members.h"
#undef FIELD_DEF
}
};
这样你只需要在一个地方添加字段。
答案 3 :(得分:0)
在单元测试中,您可以使用预期的结构大小进行static_assert检查:
static_assert( sizeof(DataType1)==16, "Structure changed. Update serialize method" );
您必须为每个平台(或仅针对一个平台)设置结构的大小(检查中的数字)。
答案 4 :(得分:0)
您可以将静态变量“version”添加到结构中,并在结构更改时增加它。
static int version = 1234;
然后在你的测试中写下
static_assert( DataType1::version == HARD_CODE_VALUE );
但是你仍然可以忘记在更改结构时更新版本,或者在更新测试时忘记添加一些新成员。
答案 5 :(得分:0)
我有类似的问题,我在使用boost :: fusion时发现了我的解决方案。 在这里,您可以遍历结构的所有成员。所以不需要再手动了。 此外,您还可以获得编译时内省的好处。因此很容易打印完整结构的内容,例如。通过liitle模板代码进入日志文件。
答案 6 :(得分:0)
craay方法是不直接使用成员。
创建聚合变量模板。创建数据成员模板。
数据成员模板采用标记结构。
覆盖data_member<tag,T>::operator^( tag )
以返回对T
的引用。 Mqybe对免费operator^( data_member< tag, T >*, tag )
现在您可以通过this^tag()
获取该成员,该成员看起来像成员访问权限。如果您创建tag
的全局实例,则甚至可以删除()
。
您的数据成员也有编译时间反映,因此您可以编写for_each_member
,并编写所有序列化代码一次,并将其用于每个struct
。
访问控制和其他类别的data_member
可以在aggregate
模板中完成。
基于标记的数据构建可以使用复杂且花哨的aggregate
构造函数来完成。
或者你可以等待真正的反思出现在C ++中,可能在十年之内。
或者,您可以将struct
转换为tuple
包装器,并使用类似上述覆盖技巧的方式使this^tag
用于基于名称的访问。
如果我们要struct foo
包含int x, y
和double d
,我们可以执行以下操作:
// boilerplate
template<typename C, std::size_t idx> struct Tag {};
template<typename C, std::size_t tag_idx>
auto operator^(C&& lhs, Tag<C, tag_idx> const&>)
-> decltype( std::get<tag_idx>( std::forward<C>(lhs) )
{ return std::get<tag_idx>( std::forward<C>(lhs); }
struct foo:std::tuple< int, int, double > {};
Tag< foo, 0 > x; // another annoying part: need to manually number them
Tag< foo, 1 > y; // we can avoid this via an aggregate trick, but
Tag< foo, 2 > d; // even that isn't all that pretty
int main() {
foo bar;
bar^x = 7;
bar^y = 3;
bar^d = 3.14;
}
一个(严重)问题是两个不同struct
中的两个成员变量共享相同的“命名空间”,如果它们具有相同的名称则会发生冲突。