Skip to content

从零开始的 CRTP 与 RTTI 实战(与 Template.md 互补)

导读

  • 本文与《C++/WinRT 原理及实践 —— 模板元编程在 WinRT 中的实现》(Template.md)互补:那一篇重在 COM/ABI 调用链与模板生成代码;本文侧重“编译期多态(CRTP)与运行时多态(RTTI)”的取舍与落地实践,尤其在 C++/WinRT 中正确替代 dynamic_cast 的方式。

你将获得

  • 何时用 CRTP,何时用 RTTI;C++/WinRT 中为何尽量不用 dynamic_cast
  • try_as/as 的使用与差异、运行时类名调试、接口查询与可选钩子实现
  • 性能/体积/可维护性取舍与工程化检查表

目录

  • 基础对比:CRTP vs RTTI
  • CRTP 最小闭环:可选钩子(编译期探测)
  • RTTI 在纯 C++ 与 WinRT 的边界
  • C++/WinRT 实战:try_as/QueryInterface 与调试技巧
  • 选择指南与性能要点
  • 工程化检查清单

基础对比:CRTP vs RTTI

  • CRTP(编译期多态)
    • 机制:模板静态绑定,编译期决定调用目标
    • 优点:零运行时开销、易内联、强类型
    • 典型用途:可选钩子、策略模式、生成式 API(见 Template.md 中 consume_* 模式)
  • RTTI(运行时类型识别)
    • 机制:typeid/dynamic_cast 依赖编译器生成的类型信息(/GR)
    • 优点:运行时按真实类型分发、无需模板约束
    • 代价:有运行时与二进制体积开销;跨 ABI/COM 边界无效

结论

  • C++/WinRT 的跨语言/跨模块对象遵循 COM/WinRT(IInspectable + QueryInterface),因此跨边界应使用 try_as 而非 dynamic_cast。
  • 仅在纯 C++ 对象层(非 WinRT/COM)需要运行时判断时再考虑 RTTI。

CRTP 最小闭环:可选钩子(编译期探测)

与 Template.md 的可选方法探测一致,这里给出极简版(C++20):

cpp
template <typename T>
void call_on_navigated_to(T& obj) {
    if constexpr (requires { obj.OnNavigatedTo(); }) {
        obj.OnNavigatedTo();
    }
    // 没有该方法则不生成调用代码,零运行时分支
}

struct PageA { void OnNavigatedTo() {/*...*/} };
struct PageB { /* 无该方法 */ };

PageA a; PageB b;
call_on_navigated_to(a); // 调用
call_on_navigated_to(b); // 静默跳过

要点

  • if constexpr + requires 在编译期裁剪无效分支,实现“可选钩子”
  • 无虚表,无 override;参见 Template.md 中“为什么不能 override OnNavigatedTo”

RTTI 在纯 C++ 与 WinRT 的边界

  • 纯 C++(单一编译单元/无 ABI 跨越)
    • dynamic_cast、typeid 有效,依赖 /GR 启用 RTTI
    • 用于库内对象图的安全 downcast 或调试输出
  • WinRT/COM 对象(IInspectable、接口投影)
    • dynamic_cast 无法跨 ABI 工作(不同语言/编译器边界)
    • 正确方式:QueryInterface 语义,即 C++/WinRT 的 obj.try_as<>()
    • 获取运行时类名:winrt::get_class_name(obj) 或 IInspectable::GetRuntimeClassName

对比示例

cpp
// 纯 C++
struct Base { virtual ~Base() = default; };
struct Derived : Base { void f(){} };

void foo(Base* p) {
    if (auto d = dynamic_cast<Derived*>(p)) { d->f(); }
}

// WinRT(错误做法,dynamic_cast 不起作用)
void bar(winrt::Windows::Foundation::IInspectable obj) {
    // auto d = dynamic_cast<MyRuntimeClass*>(obj); // ❌
}

// WinRT 正确做法
#include <winrt/Windows.Foundation.h>
template <typename I>
bool supports(winrt::Windows::Foundation::IInspectable const& obj) {
    return static_cast<bool>(obj.try_as<I>());
}

启用与禁用 RTTI

  • MSVC:/GR 开启 RTTI,/GR- 关闭(会影响 dynamic_cast/typeid)
  • 可在应用层保留 /GR,以便对纯 C++ 类型使用 RTTI;对 WinRT 类型一律使用 try_as

C++/WinRT 实战:try_as/QueryInterface 与调试技巧

接口查询(推荐优先使用 try_as)

cpp
using namespace winrt;
using namespace Microsoft::UI::Windowing;

void SetTallIfSupported(winrt::Windows::Foundation::IInspectable const& obj) {
    if (auto t = obj.try_as<IAppWindowTitleBar2>()) {
        t.PreferredHeightOption(TitleBarHeightOption::Tall);
    }
    // as<I>() 会在不支持时抛 hresult_no_interface,适合“必须支持”的场景
}

从 XAML Window 获取接口再调用(与 Template.md 中 PreferredHeightOption 的调用链呼应)

cpp
#include <winrt/Microsoft.UI.Xaml.h>
#include <winrt/Microsoft.UI.Windowing.h>

void Configure(winrt::Microsoft::UI::Xaml::Window const& w) {
    auto aw = w.AppWindow();
    if (auto tb2 = aw.TitleBar().try_as<IAppWindowTitleBar2>()) {
        tb2.PreferredHeightOption(TitleBarHeightOption::Standard);
    }
}

运行时类名与接口 GUID(调试定位)

cpp
#include <winrt/Windows.Foundation.h>
#include <winrt/base.h>

template <typename T>
void DebugType(T const& obj) {
    // WinRT 运行时类名(如 Microsoft.UI.Windowing.AppWindowTitleBar)
    auto name = winrt::get_class_name(obj);
    // 某接口的 GUID(编译期已知)
    auto iid  = winrt::guid_of<Microsoft::UI::Windowing::IAppWindowTitleBar2>();
    (void)name; (void)iid;
}

底层 QueryInterface(需要时)

cpp
#include <winrt/base.h>
template <typename I, typename T>
winrt::com_ptr<std::remove_pointer_t<winrt::impl::abi_t<I>>> qi(T const& obj) {
    winrt::com_ptr<winrt::impl::abi_t<I>> ptr;
    winrt::check_hresult(winrt::get_unknown(obj)->QueryInterface(winrt::guid_of<I>(), ptr.put_void()));
    return ptr;
}

统一适配器:同时支持 WinRT 接口查询与纯 C++ RTTI

cpp
#include <type_traits>
#include <winrt/Windows.Foundation.h>

template <typename To, typename From>
auto as_or_null(From& obj)
{
    using Decay = std::decay_t<From>;
    if constexpr (std::is_convertible_v<Decay, winrt::Windows::Foundation::IInspectable>) {
        return obj.try_as<To>(); // WinRT:返回 To(可能为空)
    } else {
        if constexpr (std::is_pointer_v<Decay>) {
            return dynamic_cast<To>(obj); // 纯 C++:To 必须是指针目标
        } else {
            return dynamic_cast<To*>(&obj);
        }
    }
}

选择指南与性能要点

  • 先选 CRTP(编译期)
    • 可选钩子、策略/策略组合、静态派发的生成式 API
    • 优点:零成本、强类型;缺点:编译时间增加、接口变化需重编译
  • WinRT 对象的“类型判断/转换”
    • 一律使用 try_as/as(COM QI 语义),不要 dynamic_cast
    • try_as 返回空对象,不抛异常;as 不支持时抛 hresult_no_interface
  • 纯 C++ 对象的运行时派发
    • 需要时启用 /GR,用 dynamic_cast/typeid;性能敏感路径避免频繁使用
  • 调试与诊断
    • WinRT:get_class_name + guid_of;纯 C++:typeid(T).name()(记得 /GR)

体感性能

  • CRTP 调用多为内联,分支在编译期裁剪
  • try_as 一次 QI 代价可忽略(缓存接口指针可进一步降低开销)
  • dynamic_cast 需 RTTI 支持,有一定常数开销与体积成本

工程化检查清单

  • WinRT 侧
    • 接口判断/转换:统一 try_as/as,不混用 dynamic_cast
    • 调试:使用 get_class_name(obj),记录 guid_of<I>()
    • 错误处理:as 失败的异常抓取或改用 try_as
  • CRTP 侧
    • 可选钩子使用 if constexpr + requires(C++20);老编译器用 SFINAE 退化
    • 避免与 virtual/override 混搭造成误解(详见 Template.md)
  • 构建与选项
    • C++20(if constexpr/requires)、/permissive-、/EHsc
    • RTTI:按需开启 /GR(仅纯 C++ 场景);链路中 WinRT 对象不依赖 RTTI
    • 跨模块:避免在 DLL 边界传递非 POD 的 C++ 类型;WinRT 类型经 IInspectable 安全跨越
  • 性能与稳定性
    • 高频路径缓存接口(持有 I... 成员而非每次 try_as)
    • 异常策略明确:禁用异常路径可统一 try_as + 显式分支

结语

  • CRTP 与 RTTI 并非对立:在 C++/WinRT 中,跨边界对象选 QueryInterface(try_as),框架扩展点选 CRTP。把运行时开销留给不得不做的地方,把类型错误留在编译期暴露,是零开销抽象的精髓。
  • 建议先通读 Template.md,再回到本篇按清单做工程化收敛。