诊断直接分配

创作 API 和 C++/WinRT中所述,创建实现类型的对象时,应使用 winrt::make 系列帮助程序执行此操作。 本主题深入探讨 C++/WinRT 2.0 的一项功能,该功能帮助诊断直接在堆栈上分配实现类型对象的错误。

此类错误可能会演变为神秘莫测的崩溃或数据损坏,这些问题既难以调试又耗时。 因此,这是一项重要功能,值得了解背景。

使用MyStringable来设置场景

首先,不妨考虑一下对 IStringable的一个简单实现。

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const { return L"MyStringable"; }
};

现在,假设你需要从实现中调用一个函数,该函数需要 IStringable 作为参数。

void Print(IStringable const& stringable)
{
    printf("%ls\n", stringable.ToString().c_str());
}

问题是,MyStringable 类型 不是 一个 IStringable

  • 我们的 MyStringable 类型是 IStringable 接口的一个实现。
  • IStringable 类型是投影类型。

重要

请务必了解 实现类型投影类型之间的区别。 对于基本概念和术语,请务必阅读 使用 C++/WinRT 的 API用 C++/WinRT 编写 API

实现和投影之间的空间微妙且难以理解。 事实上,为了使实现感觉更像投影,实现会向它实现的每个投影类型提供隐式转换。 这并不意味着我们可以简单地做到这一点。

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const;
 
    void Call()
    {
        Print(this);
    }
};

相反,我们需要获取引用,以便转换运算符可用作解析调用的候选项。

void Call()
{
    Print(*this);
}

这有效。 隐式转换提供从实现类型到投影类型的转换(非常高效),这在许多方案中非常有用。 如果没有该设施,许多实现类型将会显得非常麻烦的编写。 前提是仅使用 winrt::make 函数模板(或 winrt::make_self) 来分配实现,那么一切都很好。

IStringable stringable{ winrt::make<MyStringable>() };

C++/WinRT 1.0 的潜在陷阱

不过,隐式转换可能会让你陷入困境。 请考虑此无用的辅助函数。

IStringable MakeStringable()
{
    return MyStringable(); // Incorrect.
}

甚至只是这个显然无害的声明。

IStringable stringable{ MyStringable() }; // Also incorrect.

遗憾的是,这样的代码 使用 C++/WinRT 1.0 进行编译,因为这种隐式转换。 (非常严重)的问题是,我们可能会返回一个预测类型,该类型指向一个引用计数的对象,而这个对象的后备内存位于瞬态栈上。

下面是使用 C++/WinRT 1.0 编译的其他内容。

MyStringable* stringable{ new MyStringable() }; // Very inadvisable.

原始指针是危险的和劳动密集型的 bug 来源。 如果不需要,请不要使用它们。 C++/WinRT 竭尽全力提高效率,同时也不会强迫你使用原始指针。 下面是使用 C++/WinRT 1.0 编译的其他内容。

auto stringable{ std::make_shared<MyStringable>(); } // Also very inadvisable.

这是一个在多个层面上的错误。 对于同一对象,我们有两个不同的引用计数。 Windows 运行时(以及经典 COM 之前)基于与 std::shared_ptr不兼容的内在引用计数。 std::shared_ptr 当然可以用于多种有效的用途;但是,共享 Windows 运行时(和经典 COM)对象时,则完全没有必要。 最后,它还使用 C++/WinRT 1.0 进行编译。

auto stringable{ std::make_unique<MyStringable>() }; // Highly dubious.

这再次相当可疑。 唯一所有权与 myStringable的内部引用计数 共享生存期相反。

具有 C++/WinRT 2.0 的解决方案

使用 C++/WinRT 2.0,所有这些尝试直接分配实现类型都会导致编译器错误。 这是一种最好的错误,比起神秘的运行时错误要好得多。

每当需要实现时,只需使用 winrt::makewinrt::make_self,如下所示。 现在,如果忘记这样做,您将收到编译器错误,指出这个问题,并引用一个名为 use_make_function_to_create_this_object的抽象函数。 并不完全是 static_assert,但相差无几。 不过,这是检测描述的所有错误的最可靠方法。

这确实意味着我们需要对实现施加一些轻微的约束。 鉴于我们依赖缺少替代来检测直接分配,winrt::make 函数模板必须以某种方式满足抽象虚拟函数的替代。 它通过从实现中继承一个提供重写的 final 类来完成这一操作。 有关此过程,需要注意一些事项。

首先,虚拟函数仅在调试版本中存在。 这意味着检测不会影响优化版本中 vtable 的大小。

其次,由于 winrt::make 所使用的派生类是 final,这意味着即使您先前选择不将您的实现类标记为 final,优化器仍然能够推断出的任何非虚拟化都将发生。 因此,这是一个改进。 相反,实现 无法final。 同样,这没有任何影响,因为实例化的类型将始终为 final

第三,没有任何障碍阻止你将任何虚拟函数标记为 final。 当然,C++/WinRT 与经典 COM 和实现(如 WRL)大相径庭,其中有关实现的所有内容往往都是虚拟的。 在 C++/WinRT 中,虚拟调度仅限于应用程序二进制接口 (ABI)(始终 final),并且实现方法依赖于编译时或静态多态性。 这可以避免不必要的运行时多态性,因此在 C++/WinRT 实现中,几乎没有什么必要使用虚拟函数。 这是一件非常好的事情,并导致更可预测的内联。

第四,由于 winrt::make 注入派生类,因此你的实现不能具有专用析构函数。 专用析构函数在经典 COM 实现中非常受欢迎,再次,因为所有内容都是虚拟的,并且直接处理原始指针是很常见的,因此很容易意外调用 delete 而不是 Release。 C++/WinRT 特意让你很难直接处理原始指针。 你必须 真正 去获取C++/WinRT 中的原始指针,你可以调用 delete。 值语义意味着你正在处理值和引用;很少使用指针。

因此,C++/WinRT 挑战我们先入主的概念,即编写经典 COM 代码意味着什么。 这完全合理,因为 WinRT 不是经典 COM。 经典 COM 是 Windows 运行时的程序集语言。 它不应该是你每天编写的代码。 相反,C++/WinRT 可让你编写更像新式C++的代码,并且远不像经典 COM。

重要 API