是否可以在没有对象复制或未定义行为的情况下将 Base& 转换为 Derived&?

Is it possible to convert Base& to Derived& without object copying or undefined behavior?

问题:我有一个 class (PortableFoo) 设计得非常便携。它包含一个范围 class PortableBar。周围的代码库(称为客户端 A)要求 Foo 和 Bar 都具有无法移植实现的功能,并且 Foo 的实现必须调用 Bar 的实现。以下是在 GCC 中编译和工作的解决方案,但我知道在将引用从基类转换为派生类时会调用未定义的行为:

//Code in portable codebase
class PortableFoo
{
public:
    int a = 1;
    class PortableBar
    {
    public:
        int b = 1;
    } bar;
};

//Code in Client A
#include <iostream>
class AdaptedFoo: public PortableFoo
{
public:
    int fancy_foo_func()
    {
        return a + ((AdaptedBar&)bar).fancy_bar_func();
    }
    
    class AdaptedBar: public PortableBar
    {
    public:
        int fancy_bar_func()
        {
            return b;
        }
    };
};

int main()
{
    AdaptedFoo foo;
    std::cout<<foo.fancy_foo_func(); //prints "2"
    return 0;
}

如示例中,我在ClientA 中首先简单地将对象构造为AdaptedFoo 是没有问题的,问题是AdaptedFoo.bar 仍然是PortableBar 类型。我知道该问题的三种解决方案,但每种都有明显的缺点:

  1. 上述解决方案,如果 sizeof(AdaptedBar) != sizeof(PortableBar),具有未定义的行为引用转换,可能会导致分段错误。 (在真实的代码库中,我已经测试过,如果大小不匹配,它 总是 会导致分段错误,因为我实际上存储了一个 Bars 向量。最小的例子不够复杂要显示的段错误。)
  2. 使 fancy_bar_func() 成为虚拟的,在 PortableBar 中使用一个空的实现。这只是最小示例的一个解决方案,我真正的问题有额外的复杂性,即 fancy_bar_func() 必须模板化,模板化函数不能是虚拟的。 (具体来说,它是 Boost Serialization 使用的 serialize() 函数——第三方库要求它是一个模板化函数。)
  3. 为AdaptedBar 提供一个带有签名AdaptedBar(PortableBar&) 的构造函数,允许从对PortableBar 的引用构造AdaptedBar 类型的临时对象。问题是这样的临时必须复制PortableBar的所有成员,而在现实世界中,PortableBar是一个非常大的对象,因此在序列化期间制作临时副本是不合适的。

问题:由于解决方案 2 和 3 分别未能通过编译和系统要求,未定义的行为是我目前唯一已知的有效解决方案。是否还有其他我缺少的不调用 UB 的方法?

Is it possible to convert Base& to Derived& without object copying or undefined behavior?

是的,在基引用引用动态类型的基子对象的条件下是可能的Derived。一个最小的例子:

struct Base{};
struct Derived : Base{};

Derived  d;
Base&    bref = d;
Derived& dref = static_cast<Derived&>(bref); // OK, no copy

如果条件不满足,则不可能。


至于解决你的问题,当你处理继承和多态时,答案总是:使用虚函数:

struct PortableFoo
{
    int a = 1;
    struct PortableBar
    {
        int b = 1;
    };

    virtual PortableBar& bar() = 0;
};

struct AdaptedFoo: PortableFoo
{
    int fancy_foo_func()
    {
        return a + bar().fancy_bar_func();       
    }

    virtual AdaptedBar& bar() override {
        return abar;
    };

private:
    struct AdaptedBar: PortableBar
    {
        int fancy_bar_func()
        {
            return b;
        }
    } abar;
};

您 运行 成为 public 成员变量不是一个好主意的主要原因之一。

如果对 class 的所有访问都通过函数,将访问器重载到 return 包装对底层成员的引用的适配器将是 t运行sparent 重构,并且会给你主要是你想要的。

像这样:

class PortableFoo
{
public:
  class PortableBar
  {
  public:
    int b = 1;
  };

  int get_a() { return a;}
  bar& get_bar() { return bar;}

  private:
    int a = 1;
    PortableBar bar;
};

class AdaptedFoo : public PortableFoo
{
public:    
    class AdaptedBar
    {
        PortableFoo::PortableBar& bar;

    public:
        AdaptedBar(PortableFoo::PortableBar& src_bar) : bar(src_bar) {}
        int fancy_bar_func()
        {
            return bar.b;
        }
    };

    int get_a() { return a;}
    AdaptedBar get_bar() {return AdaptedBar{PortableFoo::get_bar()};}
    
    int fancy_foo_func()
    {
        return a + get_bar().fancy_bar_func();
    }
};

对于那些来到这里搜索 Boost 序列化的人,我发现了一个特定于该问题的解决方案:根本不需要推导 class,因为所需的功能在技术上是可移植的。

在文件顶部添加一个前向声明使库所需的以下代码 100% 可移植:

namespace boost{namespace serialization{ class access;}}
class PortableBar
{
    private:
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & archive, const unsigned int version)
    {
        // archive statements with operators &, <<, >>
    }
}

这是因为该函数是模板化的,这意味着如果它从未被调用,它就永远不会被编译。只有客户端 A 有任何对 serialize() 的调用,因此只有在编译客户端 A 时才会编译 PortableBar::serialize(),并且此时必要的 Boost headers 将可用。

事后看来,这可能是库作者的一个非常有意的选择,我对无法将函数虚拟化感到沮丧是因为他们实际上提供了一种在可移植代码中使用库的更好方法。