Can a C++ Class Include Itself as an Member

Can a c++ class include itself as an member?

No, because the object would be infinitely large (because every Node has as members two other Node objects, which each have as members two other Node objects, which each... well, you get the point).

You can, however, have a pointer to the class type as a member variable:

class Node {
char *cargo;
Node* left; // I'm not a Node; I'm just a pointer to a Node
Node* right; // Same here
};

Can a C++ class include a member of its own type

Yes, it's doable in C++. But the syntax would be a little different:

  1. this-> instead of this.

  2. private:/public: instead of private/public per member

  3. remember to have ; at the end of the class

  4. A* as member (or std::uniqe_ptr<A> or std::shared_ptr<A> or std::weak_ptr<A>).


Items 1-3 are merely syntax. Item 4 is an essential difference between Java and C++:

  • In Java an object variable is a reference to the object while in C++ an object variable is a value. This is why you can't hold in C++ a direct member of yourself, as is, the size of the object would be infinite (A holding an actual value of A, holding an actual value of A, ... recursively).

    In Java when A holds an A, it just holds a reference to the other A (yes, you can still access recursively the referenced A, but it is not part of your size, you just hold a reference to it, it is stored elsewhere in memory. The addition to your size is just the size of a reference).

    You can achieve similar semantics in C++ with reference variables or pointers, by adding & for a reference or * for a pointer:

    A& a2 = a1; // a2 is a reference to A, assigned with a reference to a1
    // note that a1 above is assumed to be also of type A&

    A* a2 = a1; // a2 is a pointer to A, assigned with the address stored in a1
    // note that a1 above is assumed to be also of type A*
  • Java Garbage Collector reclaims unused memory while in C++ the programmer needs to handle that, possibly with C++ tools such as smart pointers.

  • Java Garbage Collector reclaims unused memory via Trace by Reachability, C++ smart pointers are based on scope lifetime. Additionally, C++ shared_ptr is based on reference counting which has its advantages, but is subject to reference cycles possible leak of memory, which should be avoided with proper design of your code.


The C++ version of "holding myself" may look like any of the below (or variations of them), depending on the exact need:

Option 1 - A holds but does not own a1 and a2

class A {
A* a1 = nullptr;
A* a2 = nullptr;

public:
A* getA1(){
return a1;
}

A* getA2(){
return a2;
}

void setA1(A* a1){
this->a1 = a1;
}

void setA2(A* a2){
this->a2 = a2;
}
};

Option 2 - A owns a1 and a2 as unique resources

class A {
std::unique_ptr<A> a1 = nullptr;
std::unique_ptr<A> a2 = nullptr;

public:
A* getA1(){
return a1.get();
}

A* getA2(){
return a2.get();
}

void setA1(std::unique_ptr<A> a1){
this->a1 = std::move(a1);
}

void setA2(std::unique_ptr<A> a2){
this->a2 = std::move(a2);
}
};

Option 3 - A holds a1 and a2 as shared resources*

* need to make sure you avoid cyclic ownership leak.

class A {
std::shared_ptr<A> a1 = nullptr;
std::shared_ptr<A> a2 = nullptr;

public:
auto getA1(){
return a1;
}

auto getA2(){
return a2;
}

void setA1(std::shared_ptr<A> a1){
this->a1 = a1;
}

void setA2(std::shared_ptr<A> a2){
this->a2 = a2;
}
};

Option 4 - A holds weak pointers to a1 and a2*

* the option of std::weak_ptr is relevant in case of possible cyclic dependency, a1 and a2 are owned elsewhere and might not be alive.

class A {
std::weak_ptr<A> a1 = nullptr;
std::weak_ptr<A> a2 = nullptr;

public:
std::shared_ptr<A> getA1(){
return a1.lock();
}

std::shared_ptr<A> getA2(){
return a2.lock();
}

void setA1(std::shared_ptr<A> a1){
this->a1 = a1;
}

void setA2(std::shared_ptr<A> a2){
this->a2 = a2;
}
};

Option 4 code example: http://coliru.stacked-crooked.com/a/92d6004280fdc147


Note that using A& (reference to A) as a member, is not an option, as in C++ reference variables are stronger than Catholic wedding, they're for the lifetime of the variable without any way to reassign to another reference. And they must be assigned to a valid reference when born.

However, if a1 and a2 are known when the object is born, never change and stay alive for the duration of the object's lifetime, then the following option is also possible:

Option 5 - A holds references to a1 and a2*

* this option is mainly to show that it is possible to hold references, however in most cases a pointer option (like option 1), or a const pointer member, would be more suitable.

class A {
A& a1;
A& a2;

public:
A(A& a1, A& a2): a1(a1), a2(a2) {}

// using ref to self as a placeholder
// to allow the creation of "the first A"
A(): a1(*this), a2(*this) {}

A& getA1(){
return a1;
}

A& getA2(){
return a2;
}
};

int main() {
A a1;
A a2(a1, a1);
}

The last and final option, below, is mainly to present the possibility of going forward with option 5 and allowing the change of the reference held by A.

This option is possible since C++20. However, it is to be noted that using a pointer for this purpose would most probably be a better choice.

Option 5b - A holds references to a1 and a2, and allowing set!*

*since C++20, note that this option is mainly to show the possibility, pointers would probably be a better choice here.

class A {
// all same as in option 5
public:
void set(A& a1, A& a2){
A other(a1, a2);
// placement new that changes internal ref
// is valid since C++20
new (this) A(other);
}
};

Code for option 5b: http://coliru.stacked-crooked.com/a/43adef3bff619e99

See also: Why can I assign a new value to a reference, and how can I make a reference refer to something else?

Declaring a Class as a Member of itself

Because it's static and therefore there is only one copy of the variable instance within the AppDomain.

What you're thinking of is this:

public class Foo
{
private Foo lol = new Foo();
}

Notice, everything here is instance, not static.

As the commenters noted (long ago), this is valid syntactically, but would result in a StackOverflowException being thrown, as the assignment requires construction, and construction creates a new assignment. One triggers the other in a cycle that ends when the call stack reaches its maximum length.

In OP's example, assignment requires construction, but the assignment is triggered by the static constructor, not the instance constructor. The static constructor only executes once within an AppDomain, in order to initialize the class' Type. It isn't triggered by instance construction, and so (in OP's example) won't result in a stack overflow.

C++: Can a class has an object of it's own type?

std::vector<Cell> neighbours;

You are storing copy of neighbours. What if state of any neighbour changes? Do you need that change to be reflected into the vector? If yes, better store the pointers to the neighbours:

std::vector<std::shared_ptr<Cell>> neighbours;

A c++ class include a static member with the same type of itself. Why this pattern?

Notice that the member is a static, so it's part of the class, not of instantiated objects. This is important, because otherwise you would be trying to make a recursive member (since the member is part of the object, it also contains the same member and so on...), but this is not the case here.

The best pattern to describe this is: global variable. The static member is initialized before main() and can be accessed from any part of the program by including the header file. This is very convenient while implementing but becomes harder to handle the more complex the program gets and the longer you have to maintain it, so the general idea is to avoid this. Also, because there is no way to control the order of initialization, dependencies between different global variables can cause problems during startup.

Create an instance of a class in the class itself

The code in your second fragment cannot compile, because the class Node is not completely defined at the point where you declare its data member x of Node type.

For the sake of understanding, imagine if it could compile, it would lead to some sort of "infinite recursion structure", that would take an infinite amount of memory.
Here's the hypothetical layout of such a class object:

{
data: int
x: Node
{
data: int
x: Node
{
data: int
x: Node
{
...
}
}
}
}

The first case works because you do not need a class to be fully defined in order to declare a pointer to it.

I find that thinking of "fully defined" as "the compiler knows how big this class is" helps to reason about problems like this: the compiler needs to know how big a class is to declare an instance of it, but not to declare a pointer to it, which has the same size regardless of the class.

How does creating a instance of class inside of the class itself works?

There is absolutely no problem in creating instances of a class in the class itself. The apparent chicken-or-egg problem is solved in different ways while the program is being compiled and when it is being run.

Compile-time

When a class that creates an instance of itself is being compiled, the compiler finds that the class has a circular dependency on itself. This dependency is easy to solve: the compiler knows that the class is already being compiled so it won't try to compile it again. Instead, it pretends that the class already exists generates code accordingly.

Run-time

The biggest chicken-or-egg problem with a class creating an object of itself is when the class does not even exist yet; that is, when the class is being loaded. This problem is resolved by breaking class loading into two steps: first the class is defined and then it is initialized.

Defining means registering the class with the runtime system (JVM or CLR), so that it knows the structure that objects of the class have, and what code should be run when its constructors and methods are called.

Once the class has been defined it is initialized. This is done by initializing static members and running static initializer blocks and other things defined in the particular language. Recall that the class is already defined at this point, so the runtime knows what objects of the class look like and what code should be run to create them. This means there's no problem whatsoever to create objects of the class when initializing it.

Here's an example that illustrates how class initialization and instantiation interact in Java:

class Test {
static Test instance = new Test();
static int x = 1;

public Test() {
System.out.printf("x=%d\n", x);
}

public static void main(String[] args) {
Test t = new Test();
}
}

Let's step through how the JVM would run this program. First the JVM loads the Test class. This means that the class is first defined, so that the JVM knows that

  1. a class called Test exists and that it has a main method and a constructor, and that
  2. the Test class has two static variables, one called x and another called instance, and
  3. what is the object layout of the Test class. In other words: what an object looks like; what attributes it has. In this case Test doesn't have any instance attributes.

Now that the class is defined, it is initialized. First of all, the default value 0 or null is assigned to every static attribute. This sets x to 0. Then the JVM executes the static field initializers in the source code order. There are two:

  1. Create an instance of the Test class and assign it to instance. There are two steps to instance creation:
    1. First memory is allocated for the object. The JVM can do this because it already knows the object layout from the class definition phase.
    2. The Test() constructor is called to initialize the object. The JVM can do this because it already has the code for the constructor from the class definition phase. The constructor prints out the current value of x, which is 0.
  2. Set static variable x to 1.

Only now the class has finished loading. Notice that the JVM created an instance of the class, even though it was not fully loaded yet. You have proof of this fact because the constructor printed out the initial default value 0 for x.

Now that the JVM has loaded this class, it calls the main method to run the program. The main method creates another object of class Test - the second in the execution of the program. Again the constructor prints out the current value of x, which is now 1. The full output of the program is:

x=0
x=1

As you can see there is no chicken-or-egg problem: the separation of class loading into definition and initialization phases avoids the problem completely.

What about when an instance of the object wants to create another instance, like in the code below?

class Test {
Test buggy = new Test();
}

When you create an object of this class, again there is no inherent problem. The JVM knows how the object should be laid out in memory so it can allocate memory for it. It sets all the attributes to their default values, so buggy is set to null. Then the JVM starts initializing the object. In order to do this it must create another object of class Test. Like before, the JVM already knows how to do that: it allocates the memory, sets the attribute to null, and starts initializing the new object... which means it must create a third object of the same class, and then a fourth, a fifth, and so on, until it either runs out of stack space or heap memory.

There is no conceptual problem here mind you: this is just a common case of an infinite recursion in a badly written program. The recursion can be controlled for example using a counter; the constructor of this class uses recursion to make a chain of objects:

class Chain {
Chain link = null;
public Chain(int length) {
if (length > 1) link = new Chain(length-1);
}
}

How can a class have a member of its own type, isn't this infinite recursion?

You're only declaring the variable and not creating it. Try creating it at declaration or in the constructor and let me know what happens:

public class Abc {
private Abc p = new Abc(); // have fun!

public static void main(String[] args) {
new Abc();
}
}

Incidentally, if you don't create it in the class, but rather accept a reference to it in a getter method or a constructor parameter, your code will work just fine. This is how some linked lists work.



Related Topics



Leave a reply



Submit