1

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)

Sebastian
  • 1,834
  • 2
  • 10
  • 22
  • Possible dup [What is the curiously recurring template pattern (CRTP)?](https://stackoverflow.com/questions/4173254/what-is-the-curiously-recurring-template-pattern-crtp) – Richard Critten Dec 05 '21 at 18:03
  • With CRTP each derived class belongs to a different hierarchy. The same instantiated object cannot be cast to another derived class. Whereas this also is a static technique, I think CRTP solves something different. – Sebastian Dec 05 '21 at 18:07
  • The overhead for virtual functions is actually minimal, one table of pointers per class and one pointer to this table per object. – Mark Ransom Dec 05 '21 at 18:12
  • In your "solution", you have `View3` derived from `View2`, and `View2` derived from `View1`. If you could do that in your original example (at the very top), you'd then just write `class Complex : public View3` and not have the problem in the first place. Essentially, it looks to me like you've moved the goalposts and declared victory over a problem different from the one you originally had set out to solve. – Igor Tandetnik Dec 05 '21 at 18:47
  • The aim is to make only certain members visible in each view and hide all others. If the member variables and functions are distinct for each view, it would work. But if they are overlapping, then virtual inheritance is necessary for the first example, isn't it? – Sebastian Dec 05 '21 at 18:53
  • Why do you want to essentially have different sets of access control? What is the use case? – Passer By Dec 05 '21 at 19:00
  • For a clean class design like the general reason why interfaces (abstract base classes) or access modifiers exist at all. The same instantiated object can show one interface when communicating with one class and another interface, when communicating with another class. – Sebastian Dec 05 '21 at 19:04
  • For "Compile-Time" Interfaces just use templates and duck-typing. Virtual base classes are needed only for *Run-Time* polymorphism. It seems that you are trying to find a complex solution without having an actual problem. At least this problem is not obvious from your question... – Sergey Kolesnik Dec 05 '21 at 19:18

1 Answers1

1

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.

Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • Yes, an adapter would be providing a solution (the adapter can even be a friend). Differences are that an adapter needs a separate memory allocation and keeps the reference to the data struct and has to do one more indirection. Advantages are that the adapter is flexible (can do internal logic or store values) and is more separated from the source code. The adapter can be put into the same memory allocation (if this is a requirement) by again using a Combined Class with the data struct and all views/adapters as members. The additional references and the redirection stay. – Sebastian Dec 06 '21 at 14:45
  • @Sebastian if we are talking run-time polumorphism, there is NO way you go around indirection via a pointer, be it a pointer stored within an adapter or a virtual table. Moreover, the adapter requires no memory allocation, since it will be allocated on a stack and as a temporary. Thus the compiler will optimize it away in most cases and call your methods directly. Also, you **don't** need to make those adapters friends. This is the whole point - your base class has to know **nothing** about their existance, thus no dependencies. – Sergey Kolesnik Dec 06 '21 at 15:04
  • The adapter is not stored, but created on the fly, and every overhead of the redirection is optimized away by the compiler/optimizer. I like the flexibility, that it needs no additional resources and it is simple to use. And if the View is simple enough, the optimizer will always succeed. If the data class has private members, which should be made visible, friend (or using inner classes for the view) is still necessary (but I do not see this as disadvantage). – Sebastian Dec 06 '21 at 16:25
  • When storing the view, one should store it by value instead of storing it by reference or pointer to not introduce an additional indirection. – Sebastian Dec 06 '21 at 16:29
  • @Sebastian `If the data class has private members, which should be made visible` they should be made visible by **default**, since otherwise it is a violation of encapsulation. Also you violate LSP by breaking immutability of that private data via interfaces. I suggest you separate your data from behavior, unless you need a stateful implementation – Sergey Kolesnik Dec 06 '21 at 16:31
  • @Sebastian views are very cheap to store, they are made to be stored by values. Storing references means copying pointers and does not eliminate indirection – Sergey Kolesnik Dec 06 '21 at 16:33
  • I understand. The object can have private members, as long as no view needs them. At the site of construction all members are public, which at least one view can access. The views can be created very early and transferred far in the program without loss of optimization as it is the same pointer as if the data object itself is transferred by reference or pointer. (I was needlessly fearing that the view had to be created late and the very public data class was visible all the time.) – Sebastian Dec 06 '21 at 16:41
  • 1
    Thank you for the clarifications! And how to write verbose, but still performant code. – Sebastian Dec 06 '21 at 16:44