Home > Net >  Compile-Time Interfaces (non-virtual)
Compile-Time Interfaces (non-virtual)

Time:12-07

If you want to have different public interfaces for one and the same object, you can use virtual base classes. But those have overhead (memory and space).

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 {
}

One could cast the object to a class with different access modifiers and same size. This is often done in plain C with structs to hide implementation details. But this solution is inherently unsafe and undefined behaviour with possibly very hard-to-find bugs, as the optimizer, if it does its job, may not handle forbidden aliasing (the same memory location having different names) well. And some compilers may rearrange the memory layout, when the access modifiers are different. Casts like dynamic_cast<>, reinterpret_cast<> and bit_cast<> are only allowed for certain classes.

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

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

Now I found at least one solution, which kind of uses super classes instead of base classes as interface and seems to be legal. Is this true? Is there an easier way to get there?

Complex.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;
};

Combined.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
}

The output is:

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

The views can only see their single respective member variable (the others are protected). Casting from Combine to a base class (the 3 views) is allowed. There are no special requirements for the Complex class, not even standard-layout or default constructible.

The Complex class contains all the members and implementation, but the Combined class has to be constructed so that all the Views are static base classes.

In the example shown the views can only be created from inside the class with the view1/2/3() functions, as the inheritance is protected. One could do public inheritance, but then would have to explicitly make all members invisible to a view protected. And the chaining order of the views could be seen. But the advantage would be, that the views can be directly cast from the Combined class. This could perhaps also achieved with operator View1& conversion operators?

Destruction from a View pointer would be possible (not implemented here) as the views know the actual constructed (dynamic) class of the object (=Combined).

Those views only work for the class of an object known at compile-time, otherwise a conventional solution with virtual is necessary.

Is there an easier (legal) way for static (non-overhead) views, which are comfortable to use?

(One could always fall back to friend functions)

CodePudding user response:

Just make an adapter:

#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 - see example with -O3

This assumes that you are using views as interfaces and the interface holders do not own the underlying data.
Adaptors are cleaner since they do not force view types as dependencies on data.
If you want to hide data from an adaptor's type signature, use type erasure.

  • Related