C++ 对于新成员,是否存在编译器强制实现完整性的模式?

C++ 对于新成员,是否存在编译器强制实现完整性的模式?,c++,compilation,C++,Compilation,想象一个更大的项目,包含一些参数结构: struct pars { int foo; }; 使用此结构作为参数,可以实现其他功能,例如: // (de)serialization into different formats static pars FromString(string const &text); static string ToString(pars const &data); static pars FromFile(string const

想象一个更大的项目,包含一些参数结构:

struct pars {
    int foo;
};
使用此结构作为参数,可以实现其他功能,例如:

// (de)serialization into different formats
static pars   FromString(string const &text);
static string ToString(pars const &data);
static pars   FromFile(string const &filename);
// [...]

// comparison / calculation / verification
static bool   equals(pars l, pars r);
static pars   average(pars a, pars b);
static bool   isValid(pars p);
// [...]

// you-name-it
现在,假设需要向该结构添加一个新成员:

struct pars {
    int foo;
    int bar; // new member
};
是否存在一种设计模式来中断构建或发出警告,直到所有必要的代码位置都被修改

示例:

  • 如果我将
    intfoo
    更改为
    stringfoo
    ,我不会错过任何需要更改的代码行
  • 如果
    int-foo
    需要更改为
    unsigned int-foo
    ,我可以将
    foo
    重命名为
    foo\u
    ,并让编译器指出需要进行修改的地方
一种局部解决方案是使成员
私有
且仅可从构造函数进行设置,必须使用所有参数调用构造函数:

pars::pars(int _foo, int _bar)
 : foo(_foo), bar(_bar)
{ }
这确保了PAR的正确创建,而不是用法-因此这将捕获
FromString()
中缺少的自适应,而不是
ToString()
中缺少的自适应


单元测试只会在测试期间(我正在搜索编译时方法)发现此类问题,而且只会发现(反)序列化部分,而不是到处都在考虑新的
bar
(在比较/计算/验证/…函数中).

将流操作与流的源或目标分离

一个非常简单的例子:

#include <sstream>
#include <fstream>

struct pars
{
    int foo;
    int bar;

    static constexpr auto current_version = 2;
};

std::istream &deserialise(std::istream &is, pars &model)
{
    int version;

    is >> version;
    is >> model.foo;
    if (version > 1) {
        is >> model.bar;
    }
    return is;
}

std::ostream &serialise(std::ostream &os, const pars &model)
{
    os << model.current_version << " ";
    os << model.foo << " ";
//    a version 2 addition
    os << model.bar<< " ";
    return os;
}

static pars FromString(std::string const &text)
{
    std::istringstream iss(text);
    auto result = pars();
    deserialise(iss, result);
    return result;
}

static std::string ToString(pars const &data)
{
    std::ostringstream oss;
    serialise(oss, data);
    return oss.str();
}

static pars FromFile(std::string const &filename)
{
    auto file = std::ifstream(filename);
    auto result = pars();
    deserialise(file, result);
    return result;
}
#包括
#包括
结构部
{
int foo;
int-bar;
静态constexpr自动当前版本=2;
};
std::istream&反序列化(std::istream&is、PAR&model)
{
int版本;
是>>版本;
is>>model.foo;
如果(版本>1){
is>>model.bar;
}
回报是;
}
标准::ostream和serialise(标准::ostream和os、const-pars和model)
{

os强制执行此操作的模式将是针对每个成员的操作

选择一个名称,如
members\u of
。使用ADL和标记,使
members\u of(tag)
返回指向
T
成员的整型常量成员指针元组

这必须写一次,然后可以在很多地方使用

我将用C++17编写它,就像在14和更早版本中一样,它只是更加冗长而已

template<class T>struct tag_t{constexpr tag_t(){}};
template<class T>constexpr tag_t<t> tag{};
template<auto X>using val_t=std::integral_constant<decltype(X), X>;
template<auto X>constexpr val_k<X> val{};

struct pars {
  int foo;
  friend constexpr auto members_of( tag_t<pars> ){
    return std::make_tuple( val<&pars::foo> );
  }
};
重载
允许您重载lambda

最后编写一个foreach\u tuple\u元素

static pars   FromString(string const &text){
  pars retval;
  foreach_tuple_element( members_of(tag<pars>), overload{
    [&](val_t<&pars::foo>){
      // code to handle pars.foo
    }
  });
  return retval;
}
现在它可以编译了


具体地说,对于序列化/反序列化,两种方法都需要一个方法(其中一个参数的类型表示它是in还是out),而字符串to/from只是序列化/反序列化的一种特殊情况

template<class A, class Self,
  std::enable_if_t<std::is_same<pars, std::decay_t<Self>>{}, int> =0
>
friend void Archive(A& a, Self& self) {
  ArchiveBlock(a, archive_tag("pars"), 3, [&]{
    Archive(a, self.foo);
    Archive(a, self.bar);
  });
}
模板
好友无效存档(A&A、Self和Self){
归档块(a,归档标签(“PAR”),3、[&]{
档案(a,self.foo);
档案馆(一个自助酒吧);
});
}
这是一个统一的序列化/反序列化方法(没有上述成员指针)工作原理的示例。您可以覆盖输出流和输入流上的
primitive const&
、以及
primitive&
上的
Archive

对于几乎所有其他内容,您都使用公共结构来读取和写入归档文件。这样可以保持输入和输出的结构相同

ArchiveBlock(归档和标记,标记版本,lambda)
lambda
包装在您拥有的任何存档块结构中。例如,您的存档块的头中可能有长度信息,允许早期的反序列化程序跳过最后添加的数据。它还可以读取和写入块;写入时,它将写出块头以及之前的任何内容重新写入正文(可能会跟踪长度,并在知道长度后备份以记录长度)。在阅读时,它将确保标记存在(并根据您的选择处理丢失的标记;跳过?),如果您希望支持较老的读者阅读较新的作者所写的内容,则可以在较新的块内容上快进



在更一般的情况下,你需要保持代码与数据对齐。这个答案可能会解决问题。序列化和反序列化是非常特殊的情况,因为与C++代码的大多数位不同,你必须对所有事物的二进制布局进行验证。这就像编写库接口;需要更多的关心。G最匹配的标签,或者更好的标题。已经通过boost.serialization和许多其他方法解决了。使用
constepr
版本启发我认为OP可以在(de)中编写
静态断言序列化函数在版本号上。@AndyG这里的想法是,软件检查允许更高版本读取由早期版本编写的存档,并提供适当的默认值。问题主要不是序列化(请参见编辑)。但我喜欢
constepr
版本成员的想法(虽然
static const int
即使在C+99中也是一个constepr)与
static_assert
结合使用:它添加了很少的额外代码,没有模板魔法,而且是不言自明的。缺点:人们很容易“忘记”将
static_assert
添加到自己的代码中。问题主要不是关于序列化的(请参见编辑)。只是感兴趣:你能详细说明一下
你想要一个方法来处理这两个问题吗?@MartinHennings
static pars   FromString(string const &text){
  pars retval;
  foreach_tuple_element( members_of(tag<pars>), overload{
    [&](val_t<&pars::foo>){
      // code to handle pars.foo
    },
    [&](val_t<&pars::bar>){
      // code to handle pars.bar
    }
  });
  return retval;
}
template<class A, class Self,
  std::enable_if_t<std::is_same<pars, std::decay_t<Self>>{}, int> =0
>
friend void Archive(A& a, Self& self) {
  ArchiveBlock(a, archive_tag("pars"), 3, [&]{
    Archive(a, self.foo);
    Archive(a, self.bar);
  });
}