Reputation: 9708
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
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:
from: https://github.com/isocpp/CppCoreGuidelines/issues/1707
Upvotes: 0
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
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
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:
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. 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
Reputation: 208353
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
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
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