开始使用C++原生对象协议

原生对象协议库(libnop)提供了一个简单的工具箱,使C++序列化/反序列化变得方便、灵活和高效。

该库得名于使用C++类型(原生对象)来指定数据结构(协议)。该框架基本上教导C++编译器如何理解并生成结构化数据流,从而通过几个注解而不离开C++语言或使用其他工具(如IDL代码生成器)来指定结构化数据存储和交换的协议。

使用 libnop 进行构建

使用libnop构建就像将包含目录添加到编译器包含路径一样简单,并确保接受C++14或更高版本。以下示例演示了如何使用gcc在命令行上构建其中一个示例:

  1. $ g++ -std=c++14 -Iinclude -o out/simple_protocol_example examples/simple_protocol.cpp

基本用法

nop::Serializernop::Deserializer 是读取和写入数据的顶层类型。这些类型提供了将C++数据类型转换为序列化表示形式和从中转换的基本接口。

以下是这些类定义的 Read()Write() 方法的签名:

  1. namespace nop {
  2. template <typename Reader>
  3. template <typename T>
  4. Status<void> Deserializer<Reader>::Read(T* value);
  5. template <typename Writer>
  6. template <typename T>
  7. Status<void> Serializer<Writer>::Write(const T& value);
  8. } // namespace nop

此接口支持以下类型:

  • 标准有符号整数类型:int8_t、int16_t、int32_t 和 int64_t。
  • 与标准类型等效的常规有符号整数类型:signed char、short、int、long 和 long long。
  • 标准无符号整数类型:uint8_t、uint16_t、uint32_t 和 uint64_t。
  • 与标准类型等效的常规无符号整数类型:unsigned char、unsigned short、unsigned int、long 和 unsigned long long。
  • 没有带有signed/unsigned修饰符的字符。
  • 布尔类型。
  • 枚举和枚举类。
  • std::string。
  • 具有任何支持类型的元素的C风格数组。
  • std::array、std::pair、std::tuple 和 std::vector,其元素为任何支持类型。
  • std::map 和 std::unordered_map,其键和值为任何支持类型。
  • 具有任何支持类型的 std::reference_wrapper。
  • 具有任何支持类型的 nop::Optional。
  • 具有任何支持类型的 nop::Result
  • 具有任何支持类型的 nop::Variant
  • nop::Handle 和 nop::UniqueHandle。
  • 具有任何支持类型成员的用户定义结构。
  • 具有任何支持类型条目的用户定义表。

请注意,任何支持类型都包括用户定义类型和任何级别的嵌套。目前不支持使用 std::reference_wrapper 或 T& 的循环引用,因为簿记需要动态或临时内存,可能会影响固定内存使用情况;这个限制可能在将来得到解决。

以下示例演示了如何将这些基本类型中的一些读取和写入到 std::stringstream。有关 Reader 和 Writer 抽象的详细信息,请参阅 Reader/Writer Interfaces。

  1. #include <array>
  2. #include <cstdint>
  3. #include <iostream>
  4. #include <map>
  5. #include <sstream>
  6. #include <string>
  7. #include <vector>
  8. #include <nop/serializer.h>
  9. #include <nop/utility/die.h>
  10. #include <nop/utility/stream_reader.h>
  11. #include <nop/utility/stream_writer.h>
  12. namespace {
  13. // 将致命错误发送到 std::cerr。
  14. auto Die() { return nop::Die(std::cerr); }
  15. } // 匿名命名空间
  16. int main(int, char**) {
  17. // 在 std::stringstream 周围创建一个序列化器。
  18. using Writer = nop::StreamWriter<std::stringstream>;
  19. nop::Serializer<Writer> serializer;
  20. // 将一些数据类型写入流中。
  21. serializer.Write(10) || Die();
  22. serializer.Write(20.0f) || Die();
  23. serializer.Write(std::string{"The quick brown fox..."}) || Die();
  24. serializer.Write(std::array<std::string, 2>{{"abcdefg", "1234567"}}) || Die();
  25. serializer.Write(std::vector<std::uint64_t>{1, 2, 3, 4, 5, 6, 7}) || Die();
  26. serializer.Write(std::map<int, std::string>{{0, "abc"}, {1, "123"}}) || Die();
  27. // 在包含序列化数据的 std::stringstream 周围创建一个反序列化器。
  28. using Reader = nop::StreamReader<std::stringstream>;
  29. nop::Deserializer<Reader> deserializer{serializer.writer().take()};
  30. // 从流中读取一些数据类型。
  31. int int_value;
  32. float float_value;
  33. std::string string_value;
  34. std::array<std::string, 2> array_value;
  35. std::vector<std::uint64_t> vector_value;
  36. std::map<int, std::string> map_value;
  37. deserializer.Read(&int_value) || Die();
  38. deserializer.Read(&float_value) || Die();
  39. deserializer.Read(&string_value) || Die();
  40. deserializer.Read(&array_value) || Die();
  41. deserializer.Read(&vector_value) || Die();
  42. deserializer.Read(&map_value) || Die();
  43. return 0;
  44. }

用户定义类型

在简单情况下,写入基本类型和其容器的序列对于序列化是有用的,但是许多情况可以从更进一步的结构中受益。在上面的示例中,对 Read()Write() 调用的顺序以及传递给它们的类型隐含地定义了一个协议。相反,用户定义类型提供了一种明确指定数据流的结构或协议的手段。

有三种主要类别的用户定义类型:结构、表和值包装器。

结构在空间上是高效的,适用于

相对固定格式且其直接结构不预计会随时间而演变的数据。例如,表示3D位置、速度和其他3D向量的浮点数三分量向量是一种稳定的数据类型,适用于用户定义结构。

表在空间、执行时间和便利性上略显不足,但提供了双向二进制兼容性,允许用户定义表随时间添加和删除字段,并仍然处理来自表的不同版本的数据。

值包装器提供了一种在不向线格式添加额外开销的情况下向其他可序列化类型添加策略的方法。值包装器通过指定构造函数、运算符和访问器来为其他类型添加语义规则,控制底层类型的值。

请注意,用户定义结构可以包含用户定义表成员,反之亦然。包含表的结构仍然与旧数据完全兼容,即使其表成员发生演变,只要其自身结构保持一致。有关保持兼容性的另一种方式,请参阅可替代性部分。

用户定义结构

通过使用一些宏对常规C/C++结构或类类型进行注解,可以定义用户定义结构。注解是使用记录C++类型系统中用户定义类型信息的多个宏之一来执行的。注解纯粹是编译时的类型信息,不会影响用户定义类型的运行时内存大小。

一旦用户定义结构或类被注释,该类型就会被序列化API接受,并且可以直接传递给 Read()Write() 方法。该类型还可以嵌套在其他类型中,例如STL容器,甚至其他用户定义类型。

内部注解

注释用户定义类型的最简单且最灵活的方式是使用宏 `NOP_STRUCTURE(type, … / members /)。该宏必须在结构或类内部调用一次,并将类型名称和每个成员名称作为参数。

此宏定义了一个名为 NOP__MEMBERS 的嵌套类型,该类型存储有关结构成员的编译时信息。该宏还将负责序列化和反序列化结构成员的库类型设为友元,以便可以访问私有成员;在类的私有部分调用该宏是个好主意,以避免通过嵌套类型公开私有成员。

  1. #include <array>
  2. #include <cstddef>
  3. #include <cstdint>
  4. #include <string>
  5. #include <vector>
  6. #include <nop/structure.h>
  7. // 具有内部注解的简单结构。
  8. struct SimpleType {
  9. std::uint32_t foo;
  10. std::string bar;
  11. std::vector<std::uint32_t> baz;
  12. NOP_STRUCTURE(SimpleType, foo, bar, baz);
  13. };
  14. // 与没有注解的 SimpleType 相同。
  15. struct UnannotatedSimpleType {
  16. std::uint32_t foo;
  17. std::string bar;
  18. std::vector<std::uint32_t> baz;
  19. };
  20. // 证明注解不会增加 SimpleType 的运行时大小。
  21. static_assert(sizeof(SimpleType) == sizeof(UnannotatedSimpleType), "");
  22. // 具有嵌套用户定义类型的结构。
  23. struct NestedType {
  24. SimpleType bif;
  25. std::vector<SimpleType> fiz;
  26. NOP_STRUCTURE(NestedType, bif, fiz);
  27. };
  28. // 用户定义模板类型。
  29. template <typename T, std::size_t N>
  30. struct TemplateType {
  31. T foo;
  32. std::array<T, N> bar;
  33. // 不带参数的简写类型名称可以作为类型传递。
  34. NOP_STRUCTURE(TemplateType, foo, bar);
  35. };
  36. // 具有私有成员的类。协议类型可以使用访问控制和公共方法来执行对数据使用方式的策略。
  37. class Items {
  38. public:
  39. Items() = default;
  40. Items(std::vector<std::string> items) : items_{std::move(items)} {}
  41. Items(const Items&) = default;
  42. Items(Items&&) = default;
  43. Items& operator=(const Items&) = default;
  44. Items& operator=(Items&&) = default;
  45. const std::vector<std::string>& items() const { return items_; }
  46. bool empty() const { return items_.empty(); }
  47. explicit operator bool() const { return !empty(); }
  48. private:
  49. std::vector<std::string> items_;
  50. NOP_STRUCTURE(Items, items_);
  51. };

外部注解

在某些情况下,将用户定义类型的定义与注释分开是可取的。一个例子是在注释属于你无法或不想修改的库中的类型时。类似的情况出现在开发具有C公共API的C++库时:一些公共C结构可能对内部C++代码进行序列化时很有用。将定义和注释分开还有助于避免将 libnop 的使用暴露给外部代码。

使用 NOP_EXTERNAL_STRUCTURE(type,… /members/) 宏进行外部定义类型的注释,其功能类似于 NOP_STRUCTURE(),但有一些限制。它在结构体或类的定义之外的全局或命名空间范围内调用,通常在与要注释的类型不同的文件中。

在使用外部注释时,请牢记以下几点:

  • 只能在外部注释中指定公共成员。无法在不修改外部定义的结构或类的情况下授予对私有或受保护成员的访问权限。
  • 注释宏必须在与原始外部定义类型相同的命名空间中调用。在另一个命名空间中对类型别名进行注释将无法工作。一个出现这种情况的例子是,当一个C库被包装在C++ API中时,该库将全局类型别名为一个命名空间,以保持一致性或避免冲突;在这种情况下,必须在全局范围内使用 libnop,以便正确解析类型。

示例头文件,其中包含要进行外部注释的类型:

  1. #ifndef LIBFOO_INCLUDE_PUBLIC_EXTERNAL_TYPE_H_
  2. #define LIBFOO_INCLUDE_PUBLIC_EXTERNAL_TYPE_H_
  3. #include <stdint.h>
  4. #ifdef __cplusplus
  5. extern "C" {
  6. #endif
  7. // Externally-defined structure in a public header.
  8. typedef struct ExternalType {
  9. uint32_t a;
  10. uint8_t b[16];
  11. float c;
  12. } ExternalType;
  13. // Public function to operate on ExternalType.
  14. int DoSomethingWithExternalType(const ExternalType* external_type);
  15. #ifdef __cplusplus
  16. } /* extern "C" */
  17. #endif
  18. #endif // LIBFOO_INCLUDE_PUBLIC_EXTERNAL_TYPE_H_

示例源文件,包含外部注释:

  1. #include <errno.h>
  2. #include <nop/structure.h>
  3. #include <nop/serializer.h>
  4. #include "include/public/external_type.h"
  5. // Annotate the externally-defined structure in the private C++ source file.
  6. NOP_EXTERNAL_STRUCTURE(ExternalType, a, b, c);
  7. // Uses a hypothetical foo::Writer to send the serialized data somewhere.
  8. extern "C" int DoSomethingWithExternalType(const ExternalType* external_type) {
  9. nop::Serializer<foo::Writer> serializer;
  10. auto status = serializer.Write(*external_type);
  11. if (!status)
  12. return -EIO;
  13. // Do something with the data.
  14. status = serializer.writer().Send();
  15. if (!status)
  16. return -EIO;
  17. return 0;
  18. }

处理外部注释中的模板类型 有两种方法可以对外部定义的模板类型进行注释:

  1. 使用 NOP_EXTERNAL_STRUCTURE() 对每个所需的模板类型实例进行注释。该方法可以处理类型和非类型模板参数,但不够灵活,因为在注释时必须决定使用哪些参数。
  2. 使用 NOP_EXTERNAL_STRUCTURE() 对基础模板类型名称进行注释。这种方法更灵活,因为可以一次注释外部模板类型,然后在使用的地方,序列化器会自动识别任何实例化。缺点是不支持非类型模板参数。

示例头文件,其中包含要进行外部注释的模板类型:

  1. // ExampleTemplateHeader.h
  2. #pragma once
  3. // Template class definition
  4. template <typename T, int N>
  5. class MyTemplateClass {
  6. public:
  7. T data[N];
  8. // Other members and methods...
  9. };

示例源文件,包含对外部模板类型进行注释的方式:

  1. #ifndef LIBFOO_INCLUDE_PUBLIC_SIMPLE_TYPE_H_
  2. #define LIBFOO_INCLUDE_PUBLIC_SIMPLE_TYPE_H_
  3. namespace foo {
  4. template <typename T>
  5. struct SimpleType {
  6. T data;
  7. };
  8. } // namespace foo
  9. #endif // LIBFOO_INCLUDE_PUBLIC_SIMPLE_TYPE_H_

示例源文件,包含外部注释:

  1. #include <cstdint>
  2. #include <string>
  3. #include <nop/structure.h>
  4. #include "include/public/simple_type.h"
  5. namespace bar {
  6. // Even though this is an alias in namespace bar the annotations must be in
  7. // namespace foo. This is a consequence of how ADL resolves names, which
  8. // external annotations use to handle cross-namespace lookup.
  9. using foo::SimpleType;
  10. } // namespace bar
  11. namespace foo {
  12. // Example of annotating specific instantiations:
  13. NOP_EXTERNAL_STRUCTURE(SimpleType<std::uint32_t>, data);
  14. NOP_EXTERNAL_STRUCTURE(SimpleType<std::string>, data);
  15. // Example of annotating the base template type name:
  16. NOP_EXTERNAL_STRUCTURE(SimpleType, data);
  17. // ... code to read and write SimpleType ...
  18. } // namespace foo

逻辑缓冲区

虽然 std::vector 在许多情况下很有用,但有时候希望避免动态内存分配或限制数组或缓冲区的大小,同时仍然支持可变数量的元素。通过将固定大小的数组成员与适当宽度的整数成员组合在一起,用户定义的结构体可以实现这一点。

以下示例演示了语法:

  1. struct Foo {
  2. std::uint32_t misc;
  3. std::uint8_t data[256];
  4. std::uint8_t size;
  5. // Members are grouped with parenthesis, array followed by size.
  6. NOP_STRUCTURE(Foo, misc, (data, size));
  7. };
  8. struct Vec3 {
  9. float x;
  10. float y;
  11. float z;
  12. NOP_STRUCTURE(Vec3, x, y, z);
  13. };
  14. struct Points {
  15. std::array<Vec3, 32> points;
  16. std::size_t num_points;
  17. NOP_STRUCTURE(Points, (points, num_points));
  18. };
  19. // It is possible to have more than one logical buffer in a structure, and to
  20. // group members that are not adjacent in the structure definition.
  21. struct TwoBuffers {
  22. std::uint8_t buffer_one[4096];
  23. std::uint8_t buffer_two[4096];
  24. std::size_t size_two;
  25. std::size_t size_one;
  26. NOP_STRUCTURE(TwoBuffers, (buffer_one, size_one), (buffer_two, size_two));
  27. };

逻辑缓冲区的成员在注释用户定义的结构时,使用括号将它们分组在成员列表中。这可能不是最明确的语法,但在不过度复杂化底层宏处理规则的情况下,提供了合理的清晰度。

这种语法也同样适用于外部定义的类型,这使得可以序列化嵌套在外部结构定义中的C风格缓冲区构造。

用户定义的表格

表格是一种支持双向二进制兼容性的用户定义类型。这个特性允许表格随着时间的推移进行演变,可以添加或删除字段,同时仍然与同一表格类型的旧版本和新版本生成的数据保持兼容。

用户定义的表格的定义方式与用户定义的结构类似:主要的区别在于表格的所有成员必须是 nop::Entry 模板类型的实例,并且必须使用宏 NOP_TABLE(type, … / entry members /) 进行注释。

为了保持与不同版本数据的兼容性,有一些规则需要遵循:

  1. 每个表格条目必须具有唯一的数值 id。这在 nop::Entry 的 Id 模板参数中指定。编译时检查确保同一表格中的每个条目具有唯一的 id。
  2. 一旦使用了 id,就不能在同一表格定义中重新使用或重新分配它。一旦以前定义的条目被删除,其 id 不应在同一表格中再次使用。作为最佳实践,而不是删除旧条目,使用可选的模板参数:nop::Entry 标记它为已删除。这会停用条目,删除其存储,记录删除操作,并使编译时检查能够继续捕获 id 的意外重用。
  3. 与前一点相关,条目的类型不能更改,除非更改为可互换的类型。将条目的类型更改为不同的不兼容类型实际上与重新使用 id 相同,会破坏兼容性。
  4. 与用户定义的结构不同,表格的条目可以重新排序以提高可读性。
  5. 表格中的每个条目都有一个可选值:即每个条目可以包含一个值或为空。这有两个目的:首先,序列化时不需要写出空条目,从而在字节流中节省空间。其次,必须编写代码来处理每个条目为空的情况,这自然地解决了旧数据缺少新字段或新表格定义删除条目的情况。此外,在反序列化过程中,会跳过不识别 id 的条目。这些属性在不同版本的表格定义之间提供了完全的双向二进制兼容性,前提是遵循上述规则。

以下是定义表格类型的简单示例:

  1. #include <cstdint>
  2. #include <iostream>
  3. #include <string>
  4. #include <vector>
  5. #include <nop/base/table.h>
  6. #include <nop/serializer.h>
  7. #include <nop/utility/die.h>
  8. #include <nop/utility/stream_reader.h>
  9. #include <nop/utility/stream_writer.h>
  10. namespace {
  11. // Sends fatal errors to std::cerr.
  12. auto Die() { return nop::Die(std::cerr); }
  13. struct SimpleCustomer {
  14. nop::Entry<std::string, 0> name;
  15. nop::Entry<std::string, 1> address;
  16. nop::Entry<std::vector<std::string>, 2> phone_numbers;
  17. NOP_TABLE(SimpleCustomer, name, address, phone_numbers);
  18. };
  19. } // anonymous namespace
  20. int main(int, char**) {
  21. // Create a serializer around a std::stringstream.
  22. using Writer = nop::StreamWriter<std::stringstream>;
  23. nop::Serializer<Writer> serializer;
  24. serializer.Write(SimpleCustomer{
  25. "John Doe", "101 Somewhere St., City, State 55555", {{"408-555-1234"}}}) || Die();
  26. // Create a deserializer around a std::stringstream with the serialized data.
  27. using Reader = nop::StreamReader<std::stringstream>;
  28. nop::Deserializer<Reader> deserializer{serializer.writer().take()};
  29. SimpleCustomer customer;
  30. deserializer.Read(&customer) || Die();
  31. if (customer.name)
  32. std::cout << "Customer name: " << customer.name.get() << std::endl;
  33. if (customer.address)
  34. std::cout << "Customer address: " << customer.address.get() << std::endl;
  35. if (customer.phone_numbers) {
  36. std::cout << "Customer phone numbers: ";
  37. for (const auto& number : customer.phone_numbers.get())
  38. std::cout << number << " ";
  39. std::cout << std::endl;
  40. }
  41. return 0;
  42. }

用户定义的值包装器

值包装器是用户定义的类型,它在不影响基础类型的底层数据格式的情况下,为现有的可序列化类型添加了额外的C++策略。这种策略可以采用特定的构造函数、初始化器、运算符和访问器的形式,控制底层类型可以接受哪些值,以及该类型如何与其他代码进行交互。

通过在普通的C++结构或类类型上内部注释使用 NOP_VALUE(type, member) 宏来定义值包装器。此宏必须在结构体或类内部调用一次,它接受类型名称,后跟要包装的成员。

以下是值包装器的示例:

  1. #include <nop/value.h>
  2. // Simple template type that stores floating point values as fixed point
  3. // integer with the specified number of fractional bits. This type has
  4. // the same wire format as Integer.
  5. template <typename Integer, std::size_t FractionalBits_>
  6. class Fixed {
  7. public:
  8. enum : std::size_t {
  9. Bits = sizeof(Integer) * 8,
  10. FractionalBits = FractionalBits_,
  11. IntegralBits = Bits - FractionalBits
  12. };
  13. enum : std::size_t { Power = 1 << FractionalBits };
  14. static_assert(std::is_integral<Integer>::value, "");
  15. static_assert(FractionalBits < Bits, "");
  16. constexpr Fixed() = default;
  17. constexpr Fixed(const Fixed&) = default;
  18. constexpr Fixed(float f) { value_ = std::round(f * Power); }
  19. Fixed& operator=(const Fixed&) = default;
  20. Fixed& operator=(float f) { value_ = std::round(f * Power); }
  21. constexpr float float_value() const {
  22. return static_cast<float>(value_) / Power;
  23. }
  24. explicit constexpr operator float() const { return float_value(); }
  25. constexpr Integer value() const { return value_; }
  26. private:
  27. Integer value_;
  28. NOP_VALUE(Fixed, value_);
  29. };
  30. // A simple value wrapper around a logical buffer pair, providing a non-dynamic
  31. // alternative to std::vector<T>. This type is fungible with std::vector<T>.
  32. template <typename T, std::size_t Length>
  33. struct ArrayWrapper {
  34. std::array<T, Length> data;
  35. std::size_t count;
  36. NOP_VALUE(ArrayWrapper, (data, count));
  37. };

协议、验证和可互换性

本节深入探讨了库如何利用C++类型系统来指定数据交换协议,以及在反序列化过程中如何验证数据,以及一种称为可互换性的独特特性,该特性允许更灵活地使用类型以提高效率。

协议

为了帮助理解C++类型与定义协议的关系,考虑以下C++值表达式:

  1. std::array<std::string, 4>{{"abcd", "1234", "ABCD", "2468"}}

当此表达式被序列化时,数据如下所示:

  1. | BA 04 BD 04 61 62 63 64 BD 04 31 32 33 34 BD 04 41 42 43 44 BD 04 32 34 36 38 |

这个例子演示了数据的格式与C++类型的关系:这里的 BA 04 表示一个包含四个元素的数组,对应于类型 std::array<T, 4>。接下来是四个元素,每个元素以 BD 04 开头,表示一个四字节的字符串,对应于数组元素类型 std::string

验证

数据交换格式的一个重要特征是它是自描述的,这意味着数据的结构可以从数据本身中看出。这使得反序列化器能够使用与序列化期间相同的基于类型的协议信息验证数据流。当数据不符合数据的预期形状时,反序列化器将拒绝该流作为无效流。

考虑将上一个示例中的相同基本类型传递给反序列化器时会发生什么:

  1. std::array<std::string, 4> array_value;
  2. deserializer.Read(&array_value);

库中的模板生成了一组对有效数据流结构的期望。这个期望的模式可以可视化为:

  1. | BA 04 BD X X bytes BD Y Y bytes BD Z Z bytes BD W W bytes |

这个模式要求一个包含四个元素的数组,每个元素都必须是可变长度字符串。

请注意,不同的类型定义可能产生相同的数据模式。这个属性有一些有用的优势,下一节将对此进行探讨。

可互换性

可互换性指的是能够将一种东西交换为另一种东西的能力。在这个库的上下文中,它指的是能够交换指定相同协议的类型。可互换类型的一个简单示例是数组类型 std::array<int, 10>int[10]。这两种类型都指定了相同的协议,因此我们可以将它们相互替代,同时保持与由任何一种类型生成的数据兼容。

这个属性可以通过简单地在不同的地方使用兼容类型来隐式使用。例如,在以下示例中:

  1. using VariableBuffer = std::vector<std::uint8_t>;
  2. // 将 VariableBuffer 缓冲区写入写入器。
  3. nop::Status<void> WriteData(Writer* writer, const VariableBuffer& data) {
  4. nop::Serializer<Writer*> serializer{writer};
  5. return serializer.Write(data);
  6. }
  7. // 将固定大小数组写入写入器。
  8. template <std::size_t N>
  9. nop::Status<void> WriteData(Writer* writer, const std::uint8_t (&data)[N]) {
  10. nop::Serializer<Writer*> serializer{writer};
  11. return serializer.Write(data);
  12. }
  13. // 从读取器读取 VariableBuffer。可以读取由 WriteData 的任何重载生成的数据。
  14. nop::Status<void> ReadData(Reader* reader, VariableBuffer* data) {
  15. nop::Deserializer<Reader*> deserializer{reader};
  16. return deserializer.Read(data);
  17. }

这个示例显示了一种可以替代类型的简单情况。当数组大小很大时,先将固定数组复制到临时 std::vector 中是低效且不必要的。另一个好处是避免使用 std::vector 需要的动态内存,这在内存受限或需要非常高性能的情况下可能是理想的。

nop::IsFungible 在非常简单的情况下,类型的隐式可互换性可能足够,然而,当在不同的地方使用不兼容的类型时,破坏兼容性可能只能在运行时注意到,可能是在问题引入之后很长时间。这并不是一个好的情况,特别是在大规模、影响深远且有许多贡献者的项目中。

幸运的是,我们可以利用C++编译器来在编译时验证类型替代,确保通过破坏构建来注意到协议违规。libnop提供了几种调用这些检查的方法,这使得根据不同的要求进行适应变得容易。

编译时类型验证机制的基础是 trait 类型 nop::IsFungible<TypeA, TypeB>,在 nop/traits/is_fungible.h 中定义。这个 trait 比较两种类型,并根据两种类型是否可以相互替换且保留协议而评估为 true 或 false。

使用编译时检查设施的一种方式是使用 nop::IsFungiblestd::enable_if,根据与定义基本协议的参考类型的兼容性,有选择地启用或禁用函数重载。以下通过修改前面的示例,以在读取或写入数据时接受与协议类型兼容的任何类型,演示了这一点。这样可以在函数的使用方式上提供很大的灵活性。

  1. #include <nop/traits/is_fungible.h>
  2. // Define a type that is used as the standard "protocol" for this interchange.
  3. using Buffer = std::vector<std::uint8_t>;
  4. // Writes a type compatible with Buffer to the writer.
  5. template <typename T, typename Enabled = nop::EnableIfFungible<Buffer, T>>
  6. nop::Status<void> WriteData(Writer* writer, const T& data) {
  7. nop::Serializer<Writer*> serializer{writer};
  8. return serializer.Write(data);
  9. }
  10. // Reads a type compatible with Buffer from the reader.
  11. template <typename T, typename Enabled = nop::EnableIfFungible<Buffer, T>>
  12. nop::Status<void> ReadData(Reader* reader, T* data) {
  13. nop::Deserializer<Reader*> deserializer{reader};
  14. return deserializer.Read(data);
  15. }

这个示例的版本使用了 nop::EnableIfFungible<A, B, Return = void>,这是一个实用工具,它方便地结合了 std::enable_ifnop::IsFungible<A, B>,使启用表达式更简单、更易读。当不兼容的类型传递给任何方法时,编译器会拒绝调用,并显示有关启用表达式的错误消息。

nop::Protocol 在你想要在自己的内部或外部API中灵活性时,使用启用表达式来验证协议是有用的。然而,有时在方法或函数内部执行验证更有用或更实用;每次需要新的类型替代时编写一个模板函数可能会很不方便。幸运的是,libnop有一个简单的解决方案:nop::Protocol<ProtocolType>

nop::Protocol 是一个模板类型,提供了在调用序列化器或反序列化器读取或写入数据的地方验证类型之间兼容性的简单手段。这个模板类型的定义非常简单:

  1. namespace nop {
  2. // Implements a simple compile-time type-based protocol check. Overload
  3. // resolution for Write/Read methods succeeds if the argument passed for
  4. // serialization/deserialization is compatible with the protocol type
  5. // ProtocolType.
  6. template <typename ProtocolType>
  7. struct Protocol {
  8. template <typename Serializer, typename T,
  9. typename Enable = EnableIfFungible<ProtocolType, T>>
  10. static Status<void> Write(Serializer* serializer, const T& value) {
  11. return serializer->Write(value);
  12. }
  13. template <typename Deserializer, typename T,
  14. typename Enable = EnableIfFungible<ProtocolType, T>>
  15. static Status<void> Read(Deserializer* deserializer, T* value) {
  16. return deserializer->Read(value);
  17. }
  18. };
  19. } // namespace nop

当下例中使用了nop::Protocol类型时,通过 nop::Protocol<StringsProtocol>::Write 函数将三个字符串序列化到写入器中。它利用了该协议以及 std::tie,以在不进行不必要的复制的情况下实现兼容性。

以下是代码的详细说明:

  • StringsProtocol 是一个协议,被定义为 std::vector<std::string>

  • WriteData 函数使用此协议将三个字符串序列化到写入器中。它利用了 nop::Protocolstd::tie,以在不进行不必要复制的情况下实现兼容性。

  1. #include <nop/protocol.h>
  2. using StringsProtocol = std::vector<std::string>;
  3. nop::Status<void> WriteData(Writer* writer, const std::string& a,
  4. const std::string& b, const std::string& c) {
  5. nop::Serializer<Writer*> serializer{writer};
  6. return nop::Protocol<StringsProtocol>::Write(&serializer, std::tie(a, b, c));
  7. }

接下来是关于可互换用户定义类型的概念的说明:

  1. struct Foo {
  2. std::vector<std::uint8_t> buffer;
  3. NOP_STRUCTURE(Foo, buffer);
  4. };
  5. struct Bar {
  6. std::uint8_t data[128];
  7. std::size_t size;
  8. NOP_STRUCTURE(Bar, (data, size)); // 逻辑缓冲区也是可互换的。
  9. };
  10. static_assert(nop::IsFungible<Foo, Bar>::value, "");
  11. struct Baz {
  12. nop::Entry<Foo, 0> buffer_entry;
  13. NOP_TABLE(Baz, buffer_entry);
  14. };
  15. struct Bif {
  16. nop::Entry<Bar, 0> buffer_entry;
  17. NOP_TABLE(Bif, buffer_entry);
  18. };
  19. static_assert(nop::IsFungible<Baz, Bif>::value, "");