Search code examples
c++headergetter-setter

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


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++.


Solution

  • 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?