【问题标题】:Compile-Time Interfaces (non-virtual)编译时接口(非虚拟)
【发布时间】:2022-01-11 03:59:47
【问题描述】:

如果你想为同一个对象有不同的公共接口,你可以使用虚拟基类。但这些都有开销(内存和空间)。

class View1 {
public:
    int x;
}
class View2 : virtual public View1 {
public:
    int y;
}
class View3 {
public:
    int* a;
}
class Complex : virtual public View1, virtual public View2, virtual public View3 {
}

可以将对象转换为具有不同访问修饰符和相同大小的类。这通常在纯 C 中完成,并带有隐藏实现细节的结构。但是这个解决方案本质上是不安全和未定义的行为,可能很难找到错误,因为优化器如果完成了它的工作,可能无法很好地处理禁止的别名(相同的内存位置具有不同的名称)。当访问修饰符不同时,一些编译器可能会重新排列内存布局。像 dynamic_cast、reinterpret_cast 和 bit_cast 这样的强制转换只允许用于某些类。

class View1 {
public:
    int x;
private:
    int y;
    int* a;
}

class Complex {
public:
    int x;
    int y;
    int* a;
}

现在我找到了至少一种解决方案,哪种使用超类而不是基类作为接口,并且似乎是合法的。这是真的?有没有更简单的方法可以到达那里?

复杂.h:

#pragma once
#include <iostream>

class Complex {
protected:
    Complex(int v) : x(0), y(0), a(new int) { *a = v };
    ~Complex() { std::cout << "Values before destruction: a: " << *a << ", x: " << x << ", y: " << y << std::endl; delete a; }

    int* a;
    int x;
    int y;
};

View1.h:

#include "Complex.h"

class View1 : protected Complex {
protected:
    View1(int v) : Complex(v) {}; // forward constructor with parameter
public:
    using Complex::x;
};

View2.h:

#include "View1.h"

class View2 : protected View1 { // chain inheritance
protected:
    View2(int v) : View1(v) {};
public:
    using Complex::y;
};

View3.h:

#include "View2.h"

class View3 : protected View2 { // chain inheritance
protected:
    View3(int v) : View2(v) {};
public:
    using Complex::a;
};

组合.h:

#include "View3.h"

class Combined : protected View3 {
public:
    Combined(int v) : View3(v) {};
    View3& view3() { return *static_cast<View3*>(this); }
    View2& view2() { return *static_cast<View2*>(this); }
    View1& view1() { return *static_cast<View1*>(this); }
};

test.cpp:

#include "Combined.h"
#include <iostream>
using namespace std;

int main() {
    Combined object(6);         // object is constructed
    View1& v1 = object.view1(); // view1 only allows access to x
    View2& v2 = object.view2(); // view2 only allows access to y
    View3& v3 = object.view3(); // view3 only allows access to a
    v1.x = 10;
    v2.y = 13;
    *v3.a = 15;

    cout << sizeof(Combined) << endl;  // typically only the data members = 16 on a 64-bit system (x: 4, y: 4, a: 8)
    cout << addressof(object) << endl; // typically the object and all views have the same address, as only the access modifiers are changed
    cout << addressof(v1) << endl;
    cout << addressof(v2) << endl;
    cout << addressof(v3) << endl;

    return 0;                   // object is destructed and message shown
}

输出是:

16
0000000BF8EFFBE0
0000000BF8EFFBE0
0000000BF8EFFBE0
0000000BF8EFFBE0
Values before destruction: a: 15, x: 10, y: 13

视图只能看到它们各自的成员变量(其他的受保护)。允许从 Combine 转换为基类(3 个视图)。对 Complex 类没有特殊要求,甚至没有标准布局或默认可构造。

Complex 类包含所有成员和实现,但必须构造 Combined 类,以便所有视图都是静态基类。

在显示的示例中,视图只能从具有 view1/2/3() 函数的类内部创建,因为继承受到保护。可以进行公共继承,但必须显式地使所有成员对受保护的视图不可见。并且可以看到视图的链接顺序。但优点是,可以直接从组合类中转换视图。这也许也可以通过运算符 View1& 转换运算符来实现?

由于视图知道对象的实际构造(动态)类(=Combined),因此可以从视图指针中销毁(此处未实现)。

这些视图仅适用于编译时已知的对象类,否则需要使用传统的虚拟解决方案。

对于静态(非开销)视图是否有更简单(合法)的方式,使用起来很舒服?

(总是可以退回到友元函数)

【问题讨论】:

  • 对于 CRTP,每个派生类都属于不同的层次结构。不能将同一个实例化对象强制转换为另一个派生类。虽然这也是一种静态技术,但我认为 CRTP 解决了一些不同的问题。
  • 虚函数的开销实际上是最小的,每个类一个指针表,每个对象一个指向该表的指针。
  • 在“解决方案”中,View3 派生自 View2View2 派生自 View1。如果您可以在原始示例中执行此操作(在最顶部),那么您只需编写 class Complex : public View3 并且一开始就没有问题。从本质上讲,在我看来,你已经移动了球门柱,并宣布战胜了一个与你最初打算解决的问题不同的问题。
  • 目的是使每个视图中只有某些成员可见并隐藏所有其他成员。如果每个视图的成员变量和函数都不同,那么它会起作用。但是如果它们是重叠的,那么对于第一个例子来说,虚拟继承是必要的,不是吗?

标签: c++ oop inheritance interface static-polymorphism


【解决方案1】:

只需制作一个适配器:

#include <string>
#include <iostream>

// the original data class. Does not depend on adapters, 
// thus has no reasons to be changed when a new adapter is added, 
// completely SRP compliant
struct data
{
    std::string str{"data"};
};

// this may be added in a completely separate header without the need 
// to ever modify the data class
class view
{
public:
  constexpr view(const data& ref)
    : ref_(ref)
  {}

  const std::string& str() const
  {
      return ref_.str;
  }

private:
  const data& ref_;
};

// this function uses an interface, but doesn't own the resources
void print(view v)
{
    std::cout << v.str();
}

int main()
{
    // no heap allocation is needed for an adapter
    print(data{"data"});   
}

https://godbolt.org/z/hjEzMzYYs - 请参阅-O3 的示例

这假设您将视图用作接口,并且接口持有者不拥有基础数据。
适配器更干净,因为它们不会强制 view 类型依赖于 data
如果您想从适配器的类型签名中隐藏data,请使用类型擦除。

【讨论】:

  • 是的,适配器将提供解决方案(适配器甚至可以成为朋友)。不同之处在于适配器需要单独的内存分配并保留对数据结构的引用,并且必须再进行一次间接操作。优点是适配器灵活(可以做内部逻辑或存储值),并且与源代码分离度更高。通过再次使用具有数据结构和所有视图/适配器作为成员的组合类,可以将适配器放入相同的内存分配中(如果这是必需的)。额外的引用和重定向保持不变。
  • @Sebastian 如果我们谈论的是运行时多态性,那么您无法通过指针绕过间接寻址,无论是存储在适配器或虚拟表中的指针。此外,适配器不需要内存分配,因为它将在堆栈上分配并作为临时的。因此,在大多数情况下,编译器会对其进行优化并直接调用您的方法。此外,您不需要与这些适配器交朋友。这就是重点 - 您的基类必须对它们的存在一无所知,因此没有依赖关系。
  • 适配器不是存储的,而是动态创建的,并且重定向的每个开销都被编译器/优化器优化掉。我喜欢这种灵活性,它不需要额外的资源并且使用简单。如果 View 足够简单,优化器总是会成功。如果数据类有私有成员,应该使其可见,friend(或使用视图的内部类)仍然是必要的(但我不认为这是不利的)。
  • 在存储视图时,应该按值存储,而不是按引用或指针存储,以免引入额外的间接。
  • 感谢您的澄清!以及如何编写冗长但仍然高效的代码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-08-27
  • 2016-10-18
  • 1970-01-01
  • 2013-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多