有没有办法指定和使用属于 C++ class 的所有数据成员的列表

Is there a way to specify and use a list of all data members belonging to a C++ class

在开发 C++ 代码时,我经常发现自己试图为属于 class 的所有数据成员做一些事情。经典示例在复制构造函数和赋值运算符中。另一个地方是在实现序列化功能时。在所有这些情况下,我花了很多时间来追踪大型代码库中的错误,在这些代码库中,有人向 class 添加了一个数据成员,但没有在需要的地方将其用法添加到其中一个函数中。

对于 C++11、14、17 和 20,有许多模板编程技术在它们可以做的事情上非常复杂。不幸的是,我只了解一点模板元编程。我希望有人可以指出一种方法来指定变量列表(作为 class 成员 and/or 类型),我可以用它来帮助减少有人无意中遗漏成员的错误。我可以接受编译时和 运行 时间的惩罚,只要在构建时有一种简单的方法来指定是否使用此类检测。

概念上的用法可能如下所示:

class Widget {
    template <typename Archive> void serialize(Archive ar) {
        auto myvl = vl(); // make a new list from the one defined in the constructor
        ar(a);
        ar(x);
        myvl.pop(a);
        myvl.pop(x);

        // a run-time check that would be violated because s is still in myvl.
        if (!myvl.empty())
            throw std::string{"ill-definied serialize method; an expected variable was not used"};
        // even better would be a compile-time check
    }

private:
    int a;
    double x;
    std::string s;
    VariableList vl(a, x, s);
};

或者一些静态分析,或者...

我只是在寻找提高代码质量的方法。 感谢您的帮助。

此功能随(编译时)反射功能一起提供。 https://root.cern/blog/the-status-of-reflection/ 说说去年在技术层面的地位

Reflection is a c++23 priority,并且很可能在那里。

在此之前,我采用的一种方法是为所有此类操作编写单点故障。我称之为 as_tie:

struct Foo {
  int x,y;
  template<class Self, std::enable_if_t<std::is_same_v<Foo, std::decay_t<Self>>, bool> =true>
  friend auto as_tie(Self&& self){
    static_assert(sizeof(self)==8);
    return std::forward_as_tuple( decltype(self)(self).x, decltype(self)(self).y );
  }
  friend bool operator==(Foo const&lhs, Foo const& rhs){
    return as_tie(lhs)==as_tie(rhs);
  }
};

或类似方言。

那么你的 seializer/deserializer/etc 可以使用 as_tie,也许可以使用 foreach_tuple_element。甚至可以进行版本控制; as_tie_v2_2_0 一条过时的领带。

如果有人添加成员,sizeof 静态断言可能会触发。

如果没有反射支持,这是无法做到这一点的。另一种方法是将您自定义的 struct 转换为您的成员引用的 tuple,然后使用 std::apply 逐一操作 tuple 的元素。详情可以看CppCon 2016: "C++14 Reflections Without Macros, Markup nor External Tooling"。以下是概念:

首先,我们需要检测您自定义的 struct 字段数:

template <auto I>
struct any_type {
  template <class T> constexpr operator T& () const noexcept;
  template <class T> constexpr operator T&&() const noexcept;
};

template <class T, auto... Is>
constexpr auto detect_fields_count(std::index_sequence<Is...>) noexcept {
  if constexpr (requires { T{any_type<Is>{}...}; }) return sizeof...(Is);
  else 
    return detect_fields_count<T>(std::make_index_sequence<sizeof...(Is) - 1>{});
}

template <class T>
constexpr auto fields_count() noexcept {
  return detect_fields_count<T>(std::make_index_sequence<sizeof(T)>{});
}

然后我们可以根据fields_count特征将你的struct转化为tuple(为了说明,我只支持最多8个fields_count):

template <class S>
constexpr auto to_tuple(S& s) noexcept {
  if constexpr (constexpr auto count = fields_count<S>(); count == 8) {
    auto& [f0, f1, f2, f3, f4, f5, f6, f7] = s;
    return std::tie(f0, f1, f2, f3, f4, f5, f6, f7);
  } else if constexpr (count == 7) {
    auto& [f0, f1, f2, f3, f4, f5, f6] = s;
    return std::tie(f0, f1, f2, f3, f4, f5, f6);
  } else if constexpr (count == 6) {
    auto& [f0, f1, f2, f3, f4, f5] = s;
    return std::tie(f0, f1, f2, f3, f4, f5);
  } else if constexpr (count == 5) {
    auto& [f0, f1, f2, f3, f4] = s;
    return std::tie(f0, f1, f2, f3, f4);
  } else if constexpr (count == 4) {
    auto& [f0, f1, f2, f3] = s;
    return std::tie(f0, f1, f2, f3);
  } else if constexpr (count == 3) {
    auto& [f0, f1, f2] = s;
    return std::tie(f0, f1, f2);
  } else if constexpr (count == 2) {
    auto& [f0, f1] = s;
    return std::tie(f0, f1);
  } else if constexpr (count == 1) {
    auto& [f0] = s;
    return std::tie(f0);
  } else if constexpr (count == 0) {
    return std::tie();
  }
}

然后您可以在自己的 serialize 函数中使用此实用程序:

struct Widget {
template <typename Archive>
  void serialize(Archive ar) {    
    std::apply([ar](auto&... x) { (ar(x), ...); }, to_tuple(*this));
  }
};

请参阅 godbolt 观看现场演示。

第 1 部分,共 2 部分(请参阅下面的第 2 部分)

我决定制作一个使用 CLang's AST tree 的特殊工具。

在您处理 Windows 时,我为 Windows 编写了下一个说明。

我发现 CLang 库 (SDK) 非常面向 Linux,很难直接从 Windows 上的源代码使用它。这就是为什么我决定使用 CLang 的二进制分发来解决您的任务。

Windows 的 LLVM 可以从 github releases page, particularly current release is 11.0.1. To use it on windows you have to download LLVM-11.0.1-win64.exe 下载。将它安装到某个文件夹,在我的示例中,我将它安装到 C:/bin/llvm/.

另外Visual Studio里面有自己封装的CLang,也可以用,但是有点过时了,可能不支持很新的C++20特性。

在您的 LLVM 安装中找到 clang++.exe,对于我来说是 C:/bin/llvm/bin/clang++.exe,此路径在我的脚本中用作脚本开头的 c_clang 变量。

我使用Python编写解析工具,因为这是众所周知且流行的脚本语言。我使用我的脚本来解析 CLang AST 转储的控制台输出。您可以通过下载 from here.

安装 Python

也可以使用 CLang 的 SDK 在 C++ 级别解析和处理 AST 树,AST Visitor 实现示例 is located here,但此 SDK 可能只能在 Windows 上使用得很好。这就是为什么我选择使用二进制 Windows 分发和解析控制台输出。 Linux 下的二进制分发也可以与我的脚本一起使用。

您可以通过单击下面的 Try it online! link 在 Linux 服务器上在线试用我的脚本。

脚本可以是 运行 使用 python script.py prog.cpp,这将生成输出 prog.cpp.json,其中包含名称 spaces 和 classes 的解析树。

作为基本脚本,使用命令 clang++ -cc1 -ast-dump prog.cpp 将 .cpp 文件解析为 AST。您可以手动尝试 运行ning 命令以查看它的输出,例如部分示例输出如下所示:

..................
|-CXXRecordDecl 0x25293912570 <line:10:13, line:13:13> line:10:19 class P definition
| |-DefinitionData pass_in_registers standard_layout trivially_copyable trivial literal
| | |-DefaultConstructor exists trivial needs_implicit
| | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveConstructor exists simple trivial needs_implicit
| | |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
| | `-Destructor simple irrelevant trivial needs_implicit
| |-CXXRecordDecl 0x25293912690 <col:13, col:19> col:19 implicit class P
| |-FieldDecl 0x25293912738 <line:11:17, col:30> col:30 x 'const char *'
| `-FieldDecl 0x252939127a0 <line:12:17, col:22> col:22 y 'bool'
..............

我解析此输出以生成 JSON 输出文件。 JSON 文件将如下所示(文件的一部分):

.............
{
    "node": "NamespaceDecl",
    "name": "ns2",
    "loc": "line:3:5, line:18:5",
    "tree": [
        {
            "node": "CXXRecordDecl",
            "type": "struct",
            "name": "R",
            "loc": "line:4:9, line:6:9",
            "tree": [
                {
                    "node": "FieldDecl",
                    "type": "bool *",
                    "name": "pb",
                    "loc": "line:5:13, col:20"
                }
            ]
        },
.............

正如你所看到的JSON文件有下一个字段:node告诉CLang的节点名称,它可以是NamespaceDecl for namespace, CXXRecordDecl 用于 struct/class/union,FieldDecl 用于结构字段(成员)。如果需要,我希望您可以轻松找到开源 JSON C++ 解析器,因为 JSON 是存储结构化数据的最简单格式。

在 JSON 中还有名称为 namespace/class/field 的字段 name、类型为 class 的字段 type 或字段 loc这表示 namespace/class/field 定义文件中的位置,tree 具有子节点列表(对于名称 space 节点子节点是其他名称 spaces 或 classes , 对于 class 节点子节点是字段或其他内联 classes).

我的程序也打印到控制台简化形式,只是 classes 的列表(具有完整的限定名称,包括 namespaces)加上字段列表。对于我的示例输入 .cpp,它会打印:

ns1::ns2::R - pb
ns1::ns2::S::P - x y
ns1::ns2::S::Q - r
ns1::ns2::S - i j b

示例输入 .cpp 使用:

// Start
namespace ns1 {
    namespace ns2 {
        struct R {
            bool * pb;
        };
        struct S {
            int i, j;
            bool b;
            class P {
                char const * x;
                bool y;
            };
            class Q {
                R r;
            };
        };
    }
}

int main() {
}

我还在相当复杂的 .cpp 上测试了我的脚本,它有数千行和几十个 classes。

您接下来可以使用我的脚本 - 在您的 C++ 项目准备就绪后,您可以 运行 在您的 .cpp 文件上使用我的脚本。使用脚本输出,您可以弄清楚您有哪些 class 以及每个 class 有哪些字段。然后你可以以某种方式检查这个字段列表是否与你的序列化代码相同,你可以编写简单的宏来进行自动检查。我认为获取字段列表是您需要的主要功能。 运行 我的脚本可以是编译前的一些预处理阶段。

如果您不了解 Python 并想对我的代码提出任何改进建议,请告诉我,我会更新我的代码!

Try it online!

import subprocess, re, os, sys, json, copy, tempfile, secrets

c_file = ''
c_clang = 'C:/bin/llvm/bin/clang++.exe'

def get_ast(fname, *, enc = 'utf-8', opts = [], preprocessed = False, ignore_clang_errors = True):
    try:
        if not preprocessed:
            fnameo = fname
            r = subprocess.run([c_clang, '-cc1', '-ast-dump'] + opts + [fnameo], capture_output = True)
            assert r.returncode == 0
        else:
            with tempfile.TemporaryDirectory() as td:
                tds = str(td)
                fnameo = tds + '/' + secrets.token_hex(8).upper()
                r = subprocess.run([c_clang, '-E'] + opts + [f'-o', fnameo, fname], capture_output = True)
                assert r.returncode == 0
                r = subprocess.run([c_clang, '-cc1', '-ast-dump', fnameo], capture_output = True)
                assert r.returncode == 0
    except:
        if not ignore_clang_errors:
            #sys.stdout.write(r.stdout.decode(enc)); sys.stdout.flush()
            sys.stderr.write(r.stderr.decode(enc)); sys.stderr.flush()
            raise
        pass
    return r.stdout.decode(enc), fnameo
    
def proc_file(fpath, fout = None, *, clang_opts = [], preprocessed = False, ignore_clang_errors = True):
    def set_tree(tree, path, **value):
        assert len(path) > 0
        if len(tree) <= path[0][0]:
            tree.extend([{} for i in range(path[0][0] - len(tree) + 1)])
        if 'node' not in tree[path[0][0]]:
            tree[path[0][0]]['node'] = path[0][1]
        if 'tree' not in tree[path[0][0]] and len(path) > 1:
            tree[path[0][0]]['tree'] = []
        if len(path) > 1:
            set_tree(tree[path[0][0]]['tree'], path[1:], **value)
        elif len(path) == 1:
            tree[path[0][0]].update(value)
    def clean_tree(tree):
        if type(tree) is list:
            for i in range(len(tree) - 1, -1, -1):
                if tree[i] == {}:
                    tree[:] = tree[:i] + tree[i+1:]
            for e in tree:
                clean_tree(e)
        elif 'tree' in tree:
            clean_tree(tree['tree'])
    def flat_tree(tree, name = (), fields = ()):
        for e in tree:
            if e['node'] == 'NamespaceDecl':
                if 'tree' in e:
                    flat_tree(e['tree'], name + (e['name'],), ())
            elif e['node'] == 'CXXRecordDecl':
                if 'tree' in e:
                    flat_tree(e['tree'], name + (e['name'],), ())
            elif e['node'] == 'FieldDecl':
                fields = fields + (e['name'],)
                assert 'tree' not in e['node']
            elif 'tree' in e:
                flat_tree(e['tree'], name, ())
        if len(fields) > 0:
            print('::'.join(name), ' - ', ' '.join(fields), sep = '')
    ast, fpath = get_ast(fpath, opts = clang_opts, preprocessed = preprocessed, ignore_clang_errors = ignore_clang_errors)
    fname = os.path.basename(fpath)
    ipath, path, tree = [],(), []
    st = lambda **value: set_tree(tree, path, **value)
    inode, pindent = 0, None
    for line in ast.splitlines():
        debug = (path, line)
        if not line.strip():
            continue
        m = re.fullmatch(r'^([|`\- ]*)(\S+)(?:\s+.*)?$', line)
        assert m, debug
        assert len(m.group(1)) % 2 == 0, debug
        indent = len(m.group(1)) // 2
        node = m.group(2)
        debug = (node,) + debug
        if indent >= len(path) - 1:
            assert indent in [len(path), len(path) - 1], debug
        while len(ipath) <= indent:
            ipath += [-1]
        ipath = ipath[:indent + 1]
        ipath[indent] += 1
        path = path[:indent] + ((ipath[indent], node),)
        line_col, iline = None, None
        m = re.fullmatch(r'^.*\<((?:(?:' + re.escape(fpath) + r'|line|col)\:\d+(?:\:\d+)?(?:\, )?){1,2})\>.*$', line)
        if m: #re.fullmatch(r'^.*\<.*?\>.*$', line) and not 'invalid sloc' in line and '<<<' not in line:
            assert m, debug
            line_col = m.group(1).replace(fpath, 'line')
            if False:
                for e in line_col.split(', '):
                    if 'line' in e:
                        iline = int(e.split(':')[1])
                if 'line' not in line_col:
                    assert iline is not None, debug
                    line_col = f'line:{iline}, ' + line_col
        changed = False
        if node == 'NamespaceDecl':
            m = re.fullmatch(r'^.+?\s+?(\S+)\s*$', line)
            assert m, debug
            st(name = m.group(1))
            changed = True
        elif node == 'CXXRecordDecl' and line.rstrip().endswith(' definition') and ' implicit ' not in line:
            m = re.fullmatch(r'^.+?\s+(union|struct|class)\s+(?:(\S+)\s+)?definition\s*$', line)
            assert m, debug
            st(type = m.group(1), name = m.group(2))
            changed = True
        elif node == 'FieldDecl':
            m = re.fullmatch(r'^.+?\s+(\S+?)\s+\'(.+?)\'\s*$', line)
            assert m, debug
            st(type = m.group(2), name = m.group(1))
            changed = True
        if changed and line_col is not None:
            st(loc = line_col)
    clean_tree(tree)
    if fout is None:
        fout = fpath + '.json'
    assert fout.endswith('.json'), fout
    with open(fout, 'wb') as f:
        f.write(json.dumps(tree, indent = 4).encode('utf-8'))
    flat_tree(tree)
    
if __name__ == '__main__':
    if c_file:
        proc_file(c_file)
    else:
        assert len(sys.argv) > 1
        proc_file(sys.argv[1])

输入:

// Start
namespace ns1 {
    namespace ns2 {
        struct R {
            bool * pb;
        };
        struct S {
            int i, j;
            bool b;
            class P {
                char const * x;
                bool y;
            };
            class Q {
                R r;
            };
        };
    }
}

int main() {
}

输出:

ns1::ns2::R - pb
ns1::ns2::S::P - x y
ns1::ns2::S::Q - r
ns1::ns2::S - i j b

JSON 输出:

[
    {
        "node": "TranslationUnitDecl",
        "tree": [
            {
                "node": "NamespaceDecl",
                "name": "ns1",
                "loc": "line:2:1, line:19:1",
                "tree": [
                    {
                        "node": "NamespaceDecl",
                        "name": "ns2",
                        "loc": "line:3:5, line:18:5",
                        "tree": [
                            {
                                "node": "CXXRecordDecl",
                                "type": "struct",
                                "name": "R",
                                "loc": "line:4:9, line:6:9",
                                "tree": [
                                    {
                                        "node": "FieldDecl",
                                        "type": "bool *",
                                        "name": "pb",
                                        "loc": "line:5:13, col:20"
                                    }
                                ]
                            },
                            {
                                "node": "CXXRecordDecl",
                                "type": "struct",
                                "name": "S",
                                "loc": "line:7:9, line:17:9",
                                "tree": [
                                    {
                                        "node": "FieldDecl",
                                        "type": "int",
                                        "name": "i",
                                        "loc": "line:8:13, col:17"
                                    },
                                    {
                                        "node": "FieldDecl",
                                        "type": "int",
                                        "name": "j",
                                        "loc": "col:13, col:20"
                                    },
                                    {
                                        "node": "FieldDecl",
                                        "type": "bool",
                                        "name": "b",
                                        "loc": "line:9:13, col:18"
                                    },
                                    {
                                        "node": "CXXRecordDecl",
                                        "type": "class",
                                        "name": "P",
                                        "loc": "line:10:13, line:13:13",
                                        "tree": [
                                            {
                                                "node": "FieldDecl",
                                                "type": "const char *",
                                                "name": "x",
                                                "loc": "line:11:17, col:30"
                                            },
                                            {
                                                "node": "FieldDecl",
                                                "type": "bool",
                                                "name": "y",
                                                "loc": "line:12:17, col:22"
                                            }
                                        ]
                                    },
                                    {
                                        "node": "CXXRecordDecl",
                                        "type": "class",
                                        "name": "Q",
                                        "loc": "line:14:13, line:16:13",
                                        "tree": [
                                            {
                                                "node": "FieldDecl",
                                                "type": "ns1::ns2::R",
                                                "name": "r",
                                                "loc": "line:15:17, col:19"
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }
]

第 2 部分,共 2 部分

挖掘 CLang 的内部资源我刚刚发现有一种方法可以直接从 CLang 转储到 JSON,方法是指定 -ast-dump=json(请阅读上面的第 1 部分以进行澄清),因此第 1 部分代码不是很有用,PART2代码是一个更好的解决方案。完整的 AST 转储命令将是 clang++ -cc1 -ast-dump=json prog.cpp.

我只是写了一个简单的 Python 脚本来从 JSON 转储中提取简单的信息,几乎和 PART1 一样。在每一行上,它打印完整的 struct/class/union 名称(包括名称 spaces),然后是 space,然后由 | 字段列表分隔,每个字段都是字段类型,然后是 ; 然后是字段名。应修改脚本的第一行以更正 clang++.exe 位置的路径(阅读第 1 部分)。

下面为所有 classes 收集字段名称和类型的代码如果需要也可以在 C++ 中轻松实现。甚至在 运行 时间用于提供不同的有用元信息,用于检查所有字段是否已序列化且顺序正确。此代码仅使用 JSON 格式解析器,它适用于所有编程语言。

下一个脚本可以 运行 与 python script.py prog.cpp 的第一个脚本相同。

import subprocess, json, sys

c_file = ''
c_clang = 'C:/bin/llvm/bin/clang++.exe'

r = subprocess.run([c_clang, '-cc1', '-ast-dump=json', c_file or sys.argv[1]], check = False, capture_output = True)
text = r.stdout.decode('utf-8')
data = json.loads(text)

def flat_tree(tree, path = (), fields = ()):
    is_rec = False
    if 'kind' in tree:
        if tree['kind'] == 'NamespaceDecl':
            path = path + (tree['name'],)
        elif tree['kind'] == 'CXXRecordDecl' and 'name' in tree:
            path = path + (tree['name'],)
            is_rec = True
    if 'inner' in tree:
        for e in tree['inner']:
            if e.get('kind', None) == 'FieldDecl':
                assert is_rec
                fields = fields + ((e['name'], e.get('type', {}).get('qualType', '')),)
            else:
                flat_tree(e, path, ())
    if len(fields) > 0:
        print('::'.join(path), '|'.join([f'{e[1]};{e[0]}' for e in fields]))

flat_tree(data)

输出:

ns1::ns2::R bool *;pb
ns1::ns2::S::P const char *;x|bool;y
ns1::ns2::S::Q ns1::ns2::R;r
ns1::ns2::S int;i|int;j|bool;b

输入:

// Start
namespace ns1 {
    namespace ns2 {
        struct R {
            bool * pb;
        };
        struct S {
            int i, j;
            bool b;
            class P {
                char const * x;
                bool y;
            };
            class Q {
                R r;
            };
        };
    }
}

int main() {
}

CLang 的 AST JSON 部分示例输出:

...............
{
    "id":"0x1600853a388",
    "kind":"CXXRecordDecl",
    "loc":{
        "offset":189,
        "line":10,
        "col":19,
        "tokLen":1
    },
    "range":{
        "begin":{
            "offset":183,
            "col":13,
            "tokLen":5
        },
        "end":{
            "offset":264,
            "line":13,
            "col":13,
            "tokLen":1
        }
    },
    "name":"P",
    "tagUsed":"class",
    "completeDefinition":true,
    "definitionData":{
        "canPassInRegisters":true,
        "copyAssign":{
            "hasConstParam":true,
            "implicitHasConstParam":true,
            "needsImplicit":true,
            "trivial":true
        },
        "copyCtor":{
            "hasConstParam":true,
            "implicitHasConstParam":true,
            "needsImplicit":true,
            "simple":true,
            "trivial":true
        },
        "defaultCtor":{
            "exists":true,
            "needsImplicit":true,
            "trivial":true
        },
        "dtor":{
            "irrelevant":true,
            "needsImplicit":true,
            "simple":true,
            "trivial":true
        },
        "isLiteral":true,
        "isStandardLayout":true,
        "isTrivial":true,
        "isTriviallyCopyable":true,
        "moveAssign":{
            "exists":true,
            "needsImplicit":true,
            "simple":true,
            "trivial":true
        },
        "moveCtor":{
            "exists":true,
            "needsImplicit":true,
            "simple":true,
            "trivial":true
        }
    },
    "inner":[
        {
            "id":"0x1600853a4a8",
            "kind":"CXXRecordDecl",
            "loc":{
                "offset":189,
                "line":10,
                "col":19,
                "tokLen":1
            },
            "range":{
                "begin":{
                    "offset":183,
                    "col":13,
                    "tokLen":5
                },
                "end":{
                    "offset":189,
                    "col":19,
                    "tokLen":1
                }
            },
            "isImplicit":true,
            "name":"P",
            "tagUsed":"class"
        },
        {
            "id":"0x1600853a550",
            "kind":"FieldDecl",
            "loc":{
                "offset":223,
                "line":11,
                "col":30,
                "tokLen":1
            },
            "range":{
                "begin":{
                    "offset":210,
                    "col":17,
                    "tokLen":4
                },
                "end":{
                    "offset":223,
                    "col":30,
                    "tokLen":1
                }
            },
            "name":"x",
            "type":{
                "qualType":"const char *"
            }
        },
        {
            "id":"0x1600853a5b8",
            "kind":"FieldDecl",
            "loc":{
                "offset":248,
                "line":12,
                "col":22,
                "tokLen":1
            },
            "range":{
                "begin":{
                    "offset":243,
                    "col":17,
                    "tokLen":4
                },
                "end":{
                    "offset":248,
                    "col":22,
                    "tokLen":1
                }
            },
            "name":"y",
            "type":{
                "qualType":"bool"
            }
        }
    ]
},
...............

Is there a way to specify and use a list of all data members belonging to a C++ class

是的,如果您最近使用 GCC compiler (GCC 10 in start of 2021). Code your GCC plugin 这样做。

另请参阅 DECODER project and the Bismon software and this draft 报告。

I am just looking for a way to improve the quality of my code

考虑使用 Clang static analyzer or Frama-C++ 等工具。