clice 是基于现代 C++(C++23)的新一代 C++ 语言服务器,其目标是超越 clangd。目前 clice 正处于开发早期,尚未发布 release 版本。

clice 使用了大量现代 C++ 语法,具有极高的学习价值。本文尝试分析 clice 中使用的部分元编程技术。

Struct.h

include/Support/Struct.h 中有与结构体相关的工具,例如 member_count 函数可以获取结构体的成员数量,member_name 函数可以根据结构体成员的地址获取成员的名称。

首先分析 member_count,核心代码如下:

struct Any {
    consteval Any(std::size_t);

    template <typename T>
    consteval operator T () const;
};

template <typename T, std::size_t N>
consteval auto test() {
    return []<std::size_t... I>(std::index_sequence<I...>) {
        return requires { T{Any(I)...}; };
    }(std::make_index_sequence<N>{});
}

template <typename T, std::size_t N = 0>
consteval auto member_count() {
    if constexpr(test<T, N>() && !test<T, N + 1>()) {
        return N;
    } else {
        return member_count<T, N + 1>();
    }
}

代码首先定义了 Any 结构体。它通过一个 std::size_t 类型的值构造,并且可以隐式转换为任意类型。Any 的构造函数和类型转换函数都被标记为 consteval,这意味着它们必须在编译期被调用,在运行期的调用将会引发编译错误。这保证了相关代码一定会在编译期执行,不会造成任何运行开销。

test 函数用于检查是否能用 N (第二个模板参数)个 Any 对象构造模板类型 T(第一个模板参数)对象。用一个例子方便理解:

struct Test {
    int a;
    double b;
    char c;
};

int main() {
    constexpr auto b = test<Test, 3>();
    std::println("{}", b);
}

以上代码会输出 true

Test 对象可以用零个、一个、两个或三个 Any 对象初始化,但不能超过三个,如下:

consteval void func() {
    Test t1{Any(0)};
    Test t2{Any(0), Any(1)};
    Test t3{Any(0), Any(1), Any(2)};
    Test t4{Any(0), Any(1), Any(2), Any(4)}; // compile error
}

这比较好理解,因为 Test 结构体只有三个成员。用一个 Any 对象初始化 t1 时,Any对象会隐式转换为 int 类型,t1 剩余的两个成员将被值初始化。用两个 Any 对象初始化 t2 时,第一个 Any 对象会隐式转换为 int 类型,第二个会隐式转换为 double 类型,t2 剩余的成员 c 将被值初始化。三个 Any 对象时以此类推。当使用四个 Any 对象时,会因为初始化的值多于 Test 的成员数量而报错。

接下来看 test 函数:

template <typename T, std::size_t N>
consteval auto test() {
    return []<std::size_t... I>(std::index_sequence<I...>) {
        return requires { T{Any(I)...}; };
    }(std::make_index_sequence<N>{});
}

test 函数的主体是一个 return 语句,它返回一个 lambda 表达式的调用结果。这个 lambda 表达式接收一个 std::index_sequence<I...> 类型的参数,返回 requires { T{Any(I)...}; } 的值,在 lambda 表达式最后的括号里,传入了 std::make_index_sequence 对象作为实参,调用这个 lambda 表达式。

实参 std::make_index_sequence 对象会产生编译期的 [0, N-1] 的整数序列参数包,在 lambda 表达式中,T{Any(I)...}; 这段代码展开了参数包,形成了类似 T{Any(0), Any(1), Any(2), ... , Any(N - 1)}; 的代码。包裹在外面的 requires {} 是 C++20 新增的语法特性,叫做 requires 子句。它能够检查花括号内的代码能否通过编译,如果能通过编译,则返回 true,否则返回 false。在这里,它将检查 T{Any(0), Any(1), Any(2), ... , Any(N - 1)}; 能否通过编译。如果 N 的值小于等于结构体 T 的成员数量,那么 requires { T{Any(I)...}; } 将返回 true,否则返回 false。

现在回过头重新看这句话:test 函数用于检查是否能用 N (第二个模板参数)个 Any 对象构造模板类型 T(第一个模板参数)对象。是不是理解了?

最后看 member_count 函数

template <typename T, std::size_t N = 0>
consteval auto member_count() {
    if constexpr(test<T, N>() && !test<T, N + 1>()) {
        return N;
    } else {
        return member_count<T, N + 1>();
    }
}

这段代码通过递归寻找最大的 N,也就是 T 的成员数量。当找到最大的 N,就返回它的值,否则就将 N 加一,继续寻找。最终 member_count 能够返回结构体 T 的成员数量。

下面来分析 member_namemember_name 函数可以根据结构体成员的地址得到成员的名称。代码如下:

template <typename T>
struct wrapper {
    T value;

    constexpr wrapper(T value) : value(value) {}
};

template <wrapper T>
consteval auto member_name() {
    std::string_view name = std::source_location::current().function_name();
#if __GNUC__ && (!__clang__) && (!_MSC_VER)
    std::size_t start = name.rfind("::") + 2;
    std::size_t end = name.rfind(')');
    name = name.substr(start, end - start);
#elif __clang__
    std::size_t start = name.rfind(".") + 1;
    std::size_t end = name.rfind('}');
    name = name.substr(start, end - start);
#elif _MSC_VER
    std::size_t start = name.rfind("->") + 2;
    std::size_t end = name.rfind('}');
    name = name.substr(start, end - start);
#else
    static_assert(false, "Not supported compiler");
#endif
    if(name.rfind("::") != std::string_view::npos) {
        name = name.substr(name.rfind("::") + 2);
    }
    return name;
}

member_name 的原理比较简单粗暴,使用 std::source_location 获取当前函数的名称,从函数名称中截取出结构体成员名。

member_name 的用法如下:

static struct X {
    int a;
    int b;
} x;

static_assert(member_name<&x.a>() == "a", "Member name mismatch");
static_assert(member_name<&x.b>() == "b", "Member name mismatch");

这里有一点需要注意,那就是 member_name 的模板参数值。在模板编程中,模板参数一般是 int、std::size_t 等类型,或者是枚举值、整数字面量,但是在这里,模板参数却是结构体成员的地址。

众所周知,模板参数必须在编译期指定,而变量地址在运行期才能确定,为什么这里不会有编译错误?这是因为结构体 x 被标记为 static,拥有静态存储期,编译器可以将其地址作为编译期值。可以尝试下面的代码验证这一点:

int main() {
    static int x;
    constexpr auto addr = &x;
}

更详细的解释可以看知乎上的这篇回答

最后修改:2025 年 08 月 03 日
如果觉得我的文章对你有用,请随意赞赏