5

I am writing some test units using googletest and googlemock and I am stuck in a problem related to C++11 smart pointers and polymorphism.

Suppose you have those classes:

class A {
public:
    virtual void doThings() {...};
};

class B {
private:
    B(std::unique_ptr<A> a): a_(std::move(a)) {}
    std::unique_ptr<A> a_;
};

I want to test class B and so I create a mock for A and pass it in constructor:

class MockA: public A {
public:
    virtual ~MockA() {}
    MOCK_METHOD0(doThings, void());
};

TEST(...) {
    auto ma = std::make_unique<MockA>();
    B b(std::move(ma));

    // Set expectation on mock

    // call B method
}

The problem is this:

  • the mock in moved inside B instance and so expectation verification throws exception because ma is null.

My first attempt to solve it has been to change B as follows:

class B {
private:
    B(std::unique_ptr<A>& a): a_(a) {}
    std::unique_ptr<A>& a_;
};

Now B handles unique pointer references and does not use std::move. The test also changed (no more use of std::move):

TEST(...) {
    auto ma = std::make_unique<MockA>();
    B b(ma);

    // Set expectation on mock

    // call B method
}

Now the code does not compile because (if I understand correctly the error) references are not polymorphic (the unique pointer of MockA cannot be casted to unique pointer of A).

Am I missing any basics about smart pointers? Or is this the expected behaviour with unique pointers and so I have to rethink my classes (maybe using shared pointers)?

2
  • 1
    In the general case, you can't know what happens to the object you pass to B. If it is destroyed, any pointer or reference in the test code will be dangling, and interacting with it is UB. Passing by unique_ptr is a transfer of ownership, B gets to decide how long the A lives.
    – Caleth
    Oct 18, 2017 at 8:13
  • The error you got when you changed them to references is not because references are not polymorphic - of course they are. But you're not using a reference of type A, but rather a reference of type std::unique_ptr<A>. A type of std::unique_ptr<MockA> can't be passed by reference to a std::unique_ptr<A>, because one std::unique_ptr is not a derivative of the other. (they're distinct types) The reason it "works" in your original code is because a brand new std::unique_ptr<A> got created from the temp that you passed into the constructor - and std::unique_ptr is designed to do that.
    – hadriel
    Dec 8, 2017 at 18:48

2 Answers 2

6

The easiest way would be to keep class B exactly the same as it was before, but grab the pointer from ‘ma’ before you pass it to ‘b’ in the test case. Like so…

TEST(...){
    auto ma = std::make_unique<MockA>();
    //this will be valid even after the current unique_ptr transfers ownership
    //It will be invalidated, however, if a smart pointer deletes it
    auto ( or MockA*) rawPtr = ma.get();
    B b(ma);

    //ma now points to NULL, but rawptr is still valid
    //Run analysis on mock via rawPtr
}

Changing the signature of B in such a way would not be wise. Remember, if you want a class to have exclusive ownership of a pointer, it should have its own unique_ptr for that pointer. If you change B’s unique_ptr to a reference, then B no longer technically owns the pointer even though you likely wrote the class in such a way that it acts like it owns the pointer. This could lead to some really odd behavior/crashes.

If you expect both B and A to have ownership of the pointer, meaning that the pointer NEEDS to be valid while B and another class/function are alive/running, then you should use a shared_ptr. If the pointer only needs to be valid while ONLY B is alive, then you should use a unique_ptr and make it a member of B.

Without seeing the rest of your code, I can’t say with total certainty, but I’m pretty sure you want to keep on using a unique_ptr as a member of B in this case.

EDIT

As Caleth brought out, the entire purpose of B having a unique_ptr is that it has ownership of the pointer. So if B decides it's time to delete the ptr, that memory is now invalid and any attempt to access it via the rawPtr will lead to very bad things (Undefined behaviour, crashes, etc)

Only do this if you know how B handles the unique_ptr. Only access rawPtr up until B deletes the ptr or allows its unique_ptr to go out of scope.

2
  • 1
    The pointer (or reference) can still be invalidated by the body of B destroying it, at which point you can't use rawPtr without UB
    – Caleth
    Oct 18, 2017 at 8:14
  • Great answer! I found out exactly the same. Still we must admit this could've been better implemented in C++. Prefer not to use raw pointers.
    – jaques-sam
    Dec 16, 2021 at 14:20
5

To begin with, Ryan's answer is justifiable, because the mock is being used in a white-box testing environment, which means the programmer can see all source code, and therefore knows exactly how to violate smart pointer ownership and still be assured that the code will work correctly.

If there is a need for two objects to share data in a "Dead drop" arrangement, here is a diagram that illustrates the set up:

Diagram illustrating two objects sharing data via a "dead drop" arrangement without direct communication

The "spymaster" would be your unit test code, which controls everything. An instance of "informant" (the "dead drop") is created as a std::shared_ptr. This informant will have setter methods which will be called by the "traitor" (for receiving data), and will have getter methods so that its data can be retrieved by the "spymaster".

The "spymaster" will instantiate the "traitor" as a std::unique_ptr and hand it to the "clueless" (the code-under-test). Once handed off, the "spymaster" will not have direct access to the "traitor". Instead, it will interact with the code-under-test, and when the interaction finishes it will retrieve data from the "informant" or the "dead drop".

1
  • 1
    This sounds like unique_ptr needs to point at a Facade to the real mock. All the facade does is forward the calls to the mock.
    – Arkadiy
    Mar 20, 2019 at 15:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.