Angus Comber
Angus Comber

Reputation: 9708

Is it idiomatic to store references members in a class, and are there pitfalls?

In my place of work I see this style used extensively:-

#include <iostream>

using namespace std;

class A
{
public:
   A(int& thing) : m_thing(thing) {}
   void printit() { cout << m_thing << endl; }

protected:
   const int& m_thing; //usually would be more complex object
};


int main(int argc, char* argv[])
{
   int myint = 5;
   A myA(myint);
   myA.printit();
   return 0;
}

Is there a name to describe this idiom? I am assuming it is to prevent the possibly large overhead of copying a big complex object?

Is this generally good practice? Are there any pitfalls to this approach?

Upvotes: 126

Views: 178979

Answers (7)

AndrewBloom
AndrewBloom

Reputation: 2408

It is discouraged (and labelled almost always wrong) to use reference members.

BjarneStroustrup opened on Jul 18, 2021

I think we need a rule banning reference members. All we have is a note: (Note that using a reference member is almost always wrong.)

from: https://github.com/isocpp/CppCoreGuidelines/issues/1707

The are multiple reasons for that, some of which are quite subtle:

  • Reference has a hiden constraint that the referenced object life should be longer than the holder
  • a reference as a member makes the class non-copyable and non-movable, which isn't an obvious consequence.
  • it makes the class essentially non-default constructible (unless you are referring to some global resource of course).

from: https://github.com/isocpp/CppCoreGuidelines/issues/1707

Upvotes: 0

Guy Avraham
Guy Avraham

Reputation: 3690

Wanted to add some point that was (somewhat) introduced in manilo's (great!) answer with some code:

As David Rodríguez - dribeas mentioed (in his great answer as well!), there are two "forms" of aggragation: By pointer and by reference. Take into account that if the later is used (by reference, as in your example), then the container class can NOT have a default constructor - cause all class' members of type reference MUST be initialized at construction time and the default constructor does not do it by definition.

The below code will NOT compile if you will remove the comment from the default ctor implementation (g++ version 11.3.0 will output the below error):

error: uninitialized reference member in ‘class AggregatedClass&’ [-fpermissive]
MyClass()

#include <iostream>
using namespace std;

class AggregatedClass
{
    public:
        explicit AggregatedClass(int a) : m_a(a)
        {
            cout << "AggregatedClass::AggregatedClass - set m_a:" << m_a << endl;
        }

        void func1()
        {
            cout << "AggregatedClass::func1" << endl;
        }

        ~AggregatedClass()
        {
            cout << "AggregatedClass::~AggregatedClass" << endl;
        }

    private:
        int m_a;
};

class MyClass
{
    public:         
        explicit MyClass(AggregatedClass& obj) : m_aggregatedClass(obj)
        {
           cout << "MyClass::MyClass(AggregatedClass& obj)" << endl;
        }
    
        /* this ctor can not be compiled
        MyClass() 
        {
            cout << "MyClass::MyClass()" << endl;
        } 
        */

        void func1()
        {
            cout << "MyClass::func1" << endl;
            m_aggregatedClass.func1();
        }

        ~MyClass()
        {
            cout << "MyClass::~MyClass" << endl;
        }

    private:
        AggregatedClass& m_aggregatedClass;
};

int main(int argc, char** argv)
{
    cout << "main - start" << endl;
    AggregatedClass aggregatedObj(15);
    MyClass obj(aggregatedObj);
    obj.func1();
    cout << "main - end" << endl;
    return 0;
}

Upvotes: 2

manlio
manlio

Reputation: 18902

It's called dependency injection via constructor injection: class A gets the dependency as an argument to its constructor and saves the reference to dependent class as a private variable.

There's an interesting introduction on wikipedia.

For const-correctness I'd write:

using T = int;

class A
{
public:
  A(const T &thing) : m_thing(thing) {}
  // ...

private:
   const T &m_thing;
};

but a problem with this class is that it accepts references to temporary objects:

T t;
A a1{t};    // this is ok, but...

A a2{T()};  // ... this is BAD.

It's better to add (requires C++11 at least):

class A
{
public:
  A(const T &thing) : m_thing(thing) {}
  A(const T &&) = delete;  // prevents rvalue binding
  // ...

private:
  const T &m_thing;
};

Anyway if you change the constructor:

class A
{
public:
  A(const T *thing) : m_thing(*thing) { assert(thing); }
  // ...

private:
   const T &m_thing;
};

it's pretty much guaranteed that you won't have a pointer to a temporary.

Also, since the constructor takes a pointer, it's clearer to users of A that they need to pay attention to the lifetime of the object they pass.


Somewhat related topics are:

Upvotes: 57

Alok Save
Alok Save

Reputation: 206526

Is there a name to describe this idiom?

There is no name for this usage, it is simply known as "Reference as class member".

I am assuming it is to prevent the possibly large overhead of copying a big complex object?

Yes and also scenarios where you want to associate the lifetime of one object with another object.

Is this generally good practice? Are there any pitfalls to this approach?

Depends on your usage. Using any language feature is like "choosing horses for courses". It is important to note that every (almost all) language feature exists because it is useful in some scenario.
There are a few important points to note when using references as class members:

  • You need to ensure that the referred object is guaranteed to exist till your class object exists.
  • You need to initialize the member in the constructor member initializer list. You cannot have a lazy initialization, which could be possible in case of pointer member.
  • The compiler will not generate the copy assignment operator=() and you will have to provide one yourself. It is cumbersome to determine what action your = operator shall take in such a case. So basically your class becomes non-assignable.
  • References cannot be NULL or made to refer any other object. If you need reseating, then it is not possible with a reference as in case of a pointer.

For most practical purposes (unless you are really concerned of high memory usage due to member size) just having a member instance, instead of pointer or reference member should suffice. This saves you a whole lot of worrying about other problems which reference/pointer members bring along though at expense of extra memory usage.

If you must use a pointer, make sure you use a smart pointer instead of a raw pointer. That would make your life much easier with pointers.

Upvotes: 34

Is there a name to describe this idiom?

In UML it is called aggregation. It differs from composition in that the member object is not owned by the referring class. In C++ you can implement aggregation in two different ways, through references or pointers.

I am assuming it is to prevent the possibly large overhead of copying a big complex object?

No, that would be a really bad reason to use this. The main reason for aggregation is that the contained object is not owned by the containing object and thus their lifetimes are not bound. In particular the referenced object lifetime must outlive the referring one. It might have been created much earlier and might live beyond the end of the lifetime of the container. Besides that, the state of the referenced object is not controlled by the class, but can change externally. If the reference is not const, then the class can change the state of an object that lives outside of it.

Is this generally good practice? Are there any pitfalls to this approach?

It is a design tool. In some cases it will be a good idea, in some it won't. The most common pitfall is that the lifetime of the object holding the reference must never exceed the lifetime of the referenced object. If the enclosing object uses the reference after the referenced object was destroyed, you will have undefined behavior. In general it is better to prefer composition to aggregation, but if you need it, it is as good a tool as any other.

Upvotes: 164

Indy9000
Indy9000

Reputation: 8851

C++ provides a good mechanism to manage the life time of an object though class/struct constructs. This is one of the best features of C++ over other languages.

When you have member variables exposed through ref or pointer it violates the encapsulation in principle. This idiom enables the consumer of the class to change the state of an object of A without it(A) having any knowledge or control of it. It also enables the consumer to hold on to a ref/pointer to A's internal state, beyond the life time of the object of A. This is bad design. Instead the class could be refactored to hold a ref/pointer to the shared object (not own it) and these could be set using the constructor (Mandate the life time rules). The shared object's class may be designed to support multithreading/concurrency as the case may apply.

Upvotes: 2

Puppy
Puppy

Reputation: 146930

Member references are usually considered bad. They make life hard compared to member pointers. But it's not particularly unsual, nor is it some special named idiom or thing. It's just aliasing.

Upvotes: -3

Related Questions