C++编译时动态构建列表并在运行时作为const数组使用的可行性及实现方案咨询
嘿,你的需求我完全get到了——就是想写那种清爽的server.get<"/path">(handler)风格代码,同时在编译时用CTRE生成正则,还要把这些路由规则整成一个编译期确定的只读数组,运行时直接遍历对吧?之前你尝试的Container类栽在了constexpr里用std::vector的size()上,这很正常,因为哪怕C++20支持constexpr vector,它的大小还是运行时动态的,没法当成编译时常量来用。下面给你几个实打实的解决方案:
方案1:用模板元编程搭编译期路由列表
这种方式完全在编译期构建类型列表,不依赖任何运行时容器,完美匹配你的API风格需求:
首先我们先定义基础的路由处理结构,顺便用CTRE生成编译期正则:
#include <ctre.hpp> #include <functional> #include <array> #include <variant> // 单个路由的结构,模板参数是编译期路径字符串 template<auto Path> struct RouteHandler { // 编译期生成正则 constexpr static auto regex = ctre::regex<Path>; std::function<void()> handler; constexpr RouteHandler(std::function<void()> h) : handler(std::move(h)) {} }; // 用来装路由的编译期类型列表 template<typename... Routes> struct RouteList {}; // 辅助模板:给列表加新路由 template<typename List, typename NewRoute> struct AppendRoute; template<typename... Routes, typename NewRoute> struct AppendRoute<RouteList<Routes...>, NewRoute> { using type = RouteList<Routes..., NewRoute>; }; // 用CRTP实现链式调用的Server构建器 template<typename Impl> class ServerBuilder { public: // 就是你想要的get<路径>(handler)接口 template<auto Path, typename Handler> constexpr auto get(Handler&& handler) && { using NewRoute = RouteHandler<Path>; using NewList = typename AppendRoute<typename Impl::RouteList, NewRoute>::type; // 把之前的路由和新路由打包,返回新的构建器 return ServerBuilder<Server<NewList>>{ std::move(static_cast<Impl*>(this)->m_routes), NewRoute{std::forward<Handler>(handler)} }; } // 构建最终的Server对象 constexpr auto build() && { return static_cast<Impl*>(this)->build_server(); } }; // 空的初始构建器 struct EmptyServer { using RouteList = RouteList<>; std::variant<RouteList> m_routes; constexpr auto build_server() { return Server<RouteList<>>{}; } }; // 最终的Server类,接收编译期路由列表 template<typename... Routes> class Server<RouteList<Routes...>> { public: // 把所有路由装进std::array(编译期确定大小) constexpr Server(Routes... routes) : m_routes{std::move(routes)...} {} void listen() { // 运行时直接遍历这个const数组就行 for (const auto& route : m_routes) { // 示例:用编译期正则匹配请求路径,匹配就调用handler // if (ctre::match<decltype(route)::regex>(request_path)) { // route.handler(); // } } } private: std::array<std::variant<Routes...>, sizeof...(Routes)> m_routes; };
用的时候就完全是你想要的样子了:
int main() { auto server = EmptyServer{} .get<"/hello">([] { std::cout << "hello\n"; }) .get<"/goodbye">([] { std::cout << "goodbye\n"; }) .build(); server.listen(); }
方案2:用C++20 constexpr std::array简化实现
如果觉得模板元编程有点重,用C++20的constexpr数组是更轻量的选择,虽然API稍微绕一点,但足够简洁:
#include <ctre.hpp> #include <functional> #include <array> // 统一的路由类型,把正则和handler打包 struct Route { ctre::regex<> regex; std::function<void()> handler; }; // 辅助函数:编译期生成路由 template<auto Path, typename Handler> constexpr Route make_route(Handler&& handler) { return {ctre::regex<Path>, std::forward<Handler>(handler)}; } class Server { public: // 接收编译期确定大小的array template<size_t N> constexpr Server(const std::array<Route, N>& routes) : m_routes(routes) {} void listen() { for (const auto& route : m_routes) { // 匹配逻辑和之前一样 } } private: const std::array<Route, N> m_routes; }; // 辅助函数自动推导array大小 template<size_t N> constexpr auto make_server(const std::array<Route, N>& routes) { return Server{N, routes}; }
使用方式也很直观:
int main() { // 所有路由都在编译期初始化 constexpr auto routes = std::array{ make_route<"/hello">([] { std::cout << "hello\n"; }), make_route<"/goodbye">([] { std::cout << "goodbye\n"; }) }; auto server = make_server(routes); server.listen(); }
为啥你之前的Container类不行?
你之前的Container用了std::vector,哪怕C++20允许在constexpr块里用vector,它的size()还是运行时动态值,没法用来初始化std::array的大小(array的大小必须是编译期常量)。所以要实现编译期列表,必须用模板参数或者constexpr初始化列表这种编译期确定的方式,不能依赖运行时容器。
总结一下:如果你死磕server.get<"/path">(handler)的API风格,方案1的模板元编程是最优解;如果想省点模板代码,方案2的constexpr array足够满足需求,而且代码更易懂。两种方案都能实现编译期生成正则、运行时遍历const数组的目标。
内容的提问来源于stack exchange,提问作者Thomas

