[Windows翻译]如何从C++/WinRT投影类型转换为C++/WinRT实现类型?

401 阅读4分钟

原文地址:devblogs.microsoft.com/oldnewthing…

原文作者:devblogs.microsoft.com/oldnewthing…

发布时间:2020年8月28日

上一次,我们看了从C++/WinRT实现类型转换到对应的C++/WinRT投影类型。从投影返回到实现是比较棘手的,要靠你自己的警惕。没有先验地保证投影指针真的是指你的实现。例如,如果你只有一个IInspectable,那可能是任何人。即使你有一个具体的Sample::Class,投射的类型也可能是由其他人实现的1

但是,假设你有其他的方法可以知道 IInspectable 或其他投射类型真的是由你声称的实现所支持的。通常情况下,这是因为你自己最初把它放在那里。

// Go from the projection to the implementation.
implementation::Class*p = get_self<implementation::Class>(o);

get_self函数假定你提供的东西是由你指定的实现支持的,它返回一个指向该实现类型的指针。指针的寿命是由你获得指针的投影对象控制的,所以你现在必须把o和p都留在身边,这可能是个麻烦。

除了你最好有正确的实现之外,get_self还有一个令人讨厌的陷阱。你还必须有正确的接口!

如果你传递的对象是一个接口,那么实现就不能多次实现这个接口,否则就会在如何从这个接口转换到实现上产生歧义。这在标准C++中也是一个问题。

struct B {};
struct D1 : B {};
struct D2 : B {};
struct C : D1, D2 {};

C* c;
B* b = c; // error: 'B' is an ambiguous base of 'C'

C++/WinRT的get_self函数比C++的casts限制性更强,因为它是一个转换,而不是一个转换。你要转换的东西必须被列为实现接口之一。

struct C : implements<C, ISomething>
{
};

IInspectable something;
C* p = get_self<C>(something);
// error: 'static_cast': cannot convert from
// winrt::impl::producer<D, I, void> *' to 'D *'
// with D = C, I = IInspectable

你不能从IInspectable转换过来,因为C类没有把IInspectable列为其显式实现的接口之一。它是一个隐式实现的接口,因为IInspectable是所有Windows Runtime接口的基础)。

你可以通过先将IInspectable转换为显式实现的接口来避免这个问题。

C* p = get_self<C>(something.as<ISomething>());

为了避免记住你的对象实现了哪些接口,你可以使用default_interface helper模板类型。

C* p = get_self<C>(something.as<winrt::default_interface<C>>());

但是等等,你还没有脱离困境。

如果你误将IInspectable和Windows Runtime接口一样实现,那么你会遇到另一个歧义:如果从一个IInspectable中恢复对象,那么这个IInspectable是显式实现的IInspectable,还是作为Windows Runtime接口的基类而隐式实现的IInspectable?

struct C : implements<C, ISomething, IInspectable>
{
};

IInspectable something;
C* p = get_self<C>(something); // 50% chance of working

C++/WinRT假设你给它的IInspectable来自于显式实现的接口,它有50%的几率是正确的。如果不正确,你会拿回错误的指针,破坏内存,非常伤心。

最好的解决方法就是 "别再拿错了"。从你的显式实现接口列表中删除多余的IInspectable。

一个不那么好(但仍然有效)的修复方法是使用我们上面看到的默认接口技巧。

C* p = get_self<C>(something.as<winrt::default_interface<C>>());

注意,get_self返回的值是一个原始指针。对象的寿命仍然受你传递给 get_self 的任何东西控制。这可能会很烦人,因为你现在必须携带两样东西。你需要携带原始指针以便访问你的实现, 你需要携带管理寿命的原始对象. 这里有一个辅助函数,它可以将投影类型转换为一个引用计数的com_ptr。这样一来,你就不必携带两个对象了。当我在这里的时候,我将修正这个令人讨厌的错误 (尽管实际上,你应该通过删除多余的 IInspectable 来修正它)。

template<typename D, typename T>
winrt::com_ptr<D> as_self(T&& o)
{
    winrt::com_ptr<D> result;
    if constexpr (std::is_same_v<std::remove_reference_t<T>,
                                 winrt::Windows::Foundation::IInspectable>)
    {
        auto temp = o.as<winrt::default_interface<D>>();
        result.attach(winrt::get_self<D>(temp));
        winrt::detach_abi(temp);
    }
    else if constexpr (std::is_rvalue_reference_v<T&&>)
    {
        result.attach(winrt::get_self<D>(o));
        winrt::detach_abi(o);
    }
    else
    {
        result.copy_from(winrt::get_self<D>(o));
    }

    return result;
}

基本的想法是使用get_self来获取原始指针,并使用该指针来初始化产生的com_ptr。根据不同的情况,我们可能会偷取与指针相关联的引用计数,或者我们可能只是复制它。

如果入站参数是一个IInspectable(无论是lvalue还是rvalue引用),那么我们就处于gotcha的情况下,我们会在调用get_self之前进行一个显式转换到默认接口。我们可以使用 attach/detach 语义,因为临时的默认接口很快就要退出范围,所以我们可以偷取它的引用。

否则,入站参数是 IInspectable 以外的其他东西,所以我们不在 gotcha 的情况下。对于 rvalue 引用,我们可以使用 attach/etach 语义,因为 rvalue 引用允许我们窃取它的引用。对于lvalue引用,我们使用复制语义,因为原始引用保留了它的引用。

¹ 一个投影类型有多个实现是合法的。这实际上偶尔会发生。例如PointerPoint有不同的实现,这取决于点来自哪种设备。PointerPoint本身是一个智能指针,指向一个未知的实现。

如果你不确定这个实现是否是你的,你可以创建一个私有的标记接口来识别自己的对象。


www.deepl.com 翻译