-2

Is it possible for a C++ class to include an instance of its own type the same way we can in Java? For example, something like this:

public class A {
  private A a1;
  private A a2;
  
  A getA1(){
   return a1;
  }

  A getA2(){
   return a2;
  }

  void setA1(A a1){
   this.a1 = a1;
  }

  void setA2(A a2){
   this.a2 = a2;
  }
}

Now I want the same thing or a workaround in C++.

NotTheDr01ds
  • 15,620
  • 5
  • 44
  • 70
  • 2
    That depends. What is this *stuff* that you refer to? Can you be more specific about what effect you're trying to achieve? – cigien Aug 11 '20 at 20:00
  • 4
    related/dupe: https://stackoverflow.com/questions/2706129/can-a-c-class-include-itself-as-an-member – NathanOliver Aug 11 '20 at 20:01
  • What is the problem of doing this exactly the same way in C++? (changing the syntax accordingly of course) – Slava Aug 11 '20 at 20:01
  • 1
    Turtles all the way down. – Retired Ninja Aug 11 '20 at 20:03
  • 3
    Programming C++ in the same way that you program Java is mistaken. C++ has value semantics and does not have garbage collection. These two facts alone mean good C++ programs are stylistically very different from good Java programs. – john Aug 11 '20 at 20:08

1 Answers1

7

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);
}

Option 5a - same as option 5 but without the empty constructor.

This option is based on the fact that passing an object to its own constructor is legal.

class A {
   A& a1;
   A& a2;

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

   A& getA1(){
      return a1;
   }

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

int main() {
   A a1(a1, a1); // legal, see note above
   A a2(a1, a1);
}

Code for option 5a: http://coliru.stacked-crooked.com/a/0d73dcc0d1783b7d


The last and final option, below, is mainly to present the possibility of going forward with option 5 (or 5a) 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?

Amir Kirsh
  • 12,564
  • 41
  • 74
  • What about the members that have incomplete types? – cigien Aug 11 '20 at 20:04
  • What about the garbage collection? That's the real issue. – john Aug 11 '20 at 20:07
  • 3
    `A* as member`? Ok, but that requires changing the member functions. Also, what about incomplete types in the signature of the members? I wouldn't suggest *syntax* until the OP makes it clear what *semantics* they want. – cigien Aug 11 '20 at 20:07
  • @cigien what do you mean by "changing"? That code does not exist in C++ so there is nothing to change – Slava Aug 11 '20 at 20:08
  • @Slava Unless I'm misunderstanding the answer, it's suggesting what syntax changes are needed to make OP's code c++. I'm pointing out that making the members `A*` would need additional changes. – cigien Aug 11 '20 at 20:10
  • @cigien I think that the answer now reflects the possibilities and considerations, allowing the OP to rethink what is the semantics they need. – Amir Kirsh Aug 12 '20 at 09:02
  • @john garbage collector is addressed now – Amir Kirsh Aug 12 '20 at 09:03
  • [Option 2](https://godbolt.org/z/n6sMe9) without exposing the inner pointers. – Ted Lyngmo Aug 12 '20 at 16:02
  • I don't think option 5 is valid because how do you create the first A? – Dvir Yitzchaki Feb 18 '21 at 06:59
  • @DvirYitzchaki excellent point, I was tempted to say that [it is turtles all the way down](https://en.wikipedia.org/wiki/Turtles_all_the_way_down) - but added a _stop condition_ for _the first A_. – Amir Kirsh Feb 19 '21 at 09:20