2

I'm new to C++, with a C# background. I'm trying to use dependency injection commonly used in C# in C++ and I'm confused about the different ways to declare dependencies and pass them in and why I would use one over the other. Let's assume A depends on B.

Approach #1 - declare as object and take object

class A
{
  private:
    B _b;
  public:
    A(B b): _b(b) { }

    void Foo()
    {
        _b.Bar();
    }
}

Approach #2 - declare as object and take reference

class A
{
  private:
    B _b;
  public:
    A(B &b): _b(b) { }

    void Foo()
    {
        _b.Bar();
    }
}

Approach #3 - declare as reference and take object - scratch this - undefined behavior

class A
{
  private:
    B &_b;
  public:
    A(B b): _b(b) { }

    void Foo()
    {
        _b.Bar();
    }
}

Approach #4 - declare as reference and take reference

class A
{
  private:
    B &_b;
  public:
    A(B &b): _b(b) { }

    void Foo()
    {
        _b.Bar();
    }
}

I've tried all of the above and they all seem to work. Is there any difference between them? and if so why should I choose one over the other? my preference is #1 because I find it the most familiar and intuitive.

I've found a similar discussion discussion here but that's more about Pointers vs References.

YetAnotherCoder
  • 366
  • 1
  • 9
  • And approach 4 will cause trouble if `b` goes away before `a` does because then you have a dangling reference. – Paul Sanders Nov 17 '19 at 00:30
  • 1 and 2 are useless because of [slicing](https://stackoverflow.com/questions/274626/what-is-object-slicing). 3 is [undefined behavior](https://stackoverflow.com/questions/6441218/can-a-local-variables-memory-be-accessed-outside-its-scope), 4 could work but is simply unsafe because the reference can dangle. If you insist on using dependency injection, you should use a smart pointer – alter_igel Nov 17 '19 at 00:45
  • Thanks for the comments all. I see now that 3 just happened to work. I still don't totally get the slicing issue of #1 and #2 but I'm reading up on it. – YetAnotherCoder Nov 17 '19 at 01:22
  • @alterigel is there any difference between 1 & 2? – YetAnotherCoder Nov 17 '19 at 01:34
  • 1
    @YetAnotherCoder Both 1 & 2 store a *copy* of the object that you passed the constructor. This is probably not what you want in the first place. In any case you cannot pass classes deriving from `B` to the constructor in either case, because the copying to base class type will *slice*, meaning that only the base part of the object is copied. This practically means that you cannot pass any type besides exactly `B` in either approach 1 or 2. This also doesn't seem like how dependency injection is supposed to work. – walnut Nov 17 '19 at 01:47

2 Answers2

2

One of the main difference between C# and C++ in this regard is that in C# classes are reference types whereas in C++ they are value types.

Approach 1 and 2 are based on copy construction. This could work if the object passed to the constructor is exactly a B. But it might fails due to object slicing if you'd use a derived class of B. Approach 2 might also fail if the source object is a constant.

Approach 3: no longer considered (scratched in your question)

Approach 4: is ok, provided the object passed by reference continues to exist as long as the object. However if function B::Bar() is not a virtual function, B::Bar() would always be called in Foo(), even if you'd use a class derived from B with its own implementation of Bar().

Approach 5: use a smart pointer and clone the object passed by reference (either with a clone() member, or by using templated constructor):

class A
{
  private:
    unique_ptr<B> _b;
  public:
    template<class T>A(const T&b): _b(make_unique<T>(b)) { }

    void Foo()
    {
        _b->Bar();
    }
};

Online demo

Christophe
  • 68,716
  • 7
  • 72
  • 138
  • A reference member also makes `A` non-assignable, which may be problematic depending on how the class is supposed to be used. – walnut Nov 17 '19 at 02:07
  • 1
    @uneven_mark thanks for your edit. And indeed, reference members have a couple of drawbacks. I've also added approach 5 to overcome the problem of the injectect b object that could have a shorter lifetime. A variant could be with a shared_ptr instead of a unique_ptr to let object assignable. All this would of course assume that class B allows for copy construction. – Christophe Nov 17 '19 at 02:14
  • You can also add option with std::move for noncopyable objects. – Maciej Załucki Nov 17 '19 at 03:35
1

In your case I'd say that best solution from all mentioned is taking and storing reference. The whole point of dependency injection is using pure virtual interface without even knowing how something is implemented. It means that underneath you want vtable lookup that will execute whatever implementation is for object that is referenced. This way you can expect reference for Animal where callers will provide Elephant or Cat implementation of Animal. Moreover, if you use pure virtual interface (with at least one pure virtual function inside) you can't even pass object by value because you can't create object of pure virtual type.

As far as I remmember, C# similarly to Java distinguishes objects from builtin types and objects are passed by reference by default. In C/C++ you explicitly pass everything the way you want, so in your cases you have:

  1. Copy of temporary copy of B You make more copies than you need. If your object is big, it will cause performance loss.
  2. Copy of B If you simply want to store copy of some object it seems to be better way but you created not needed limitation here. If you make copy of an object, you won't modify original object. By passing it by non-const reference you create this limitation and you can't take const objects because of that. If you simply want to take some object by reference and store copy of it, most of the time you want const reference. Exceptions might be in case of noncopyable objects.
  3. reference to temporary copy of B (invalid)
  4. Reference to B Is completely different story. Here you don't store copy of an object but point to original one, so every change you make on this object will be visible to anyone else that has access to object you received. That's more or less how objects work by default in languages like Java. Moreover, you can use virtualization as I mentioned before. The only drawback is that you have to ensure that this object exists as long as you are using it - you are not the owner of B. You just use that but someone has control of it's existence.

As you can see, in first case Second case creates some not needed limitation. You create copy of non-const

Maciej Załucki
  • 464
  • 1
  • 4
  • 15