33

I want to pass a unique_ptr to a helper function, and I want to make sure that the helper function neither modifies the pointer, nor the pointed object. Without the unique_ptr, the solution is to have

void takePtr(AClass const * const aPtr) {
  // Do something with *aPtr. 
  // We cannot change aPtr, not *aPtr. 
}

(Well, technically, AClass const * aPtr is enough.) And I can call this with

AClass * aPtr2 = new AClass(3);
takePtr(aPtr2);

I want to instead use unique_ptr, but cannot figure out how to write this. I tried

void takeUniquePtr(unique_ptr<AClass const> const & aPtr) {
  // Access *aPtr, but should not be able to change aPtr, or *aPtr. 
}

When I call this with

unique_ptr<AClass> aPtr(new AClass(3));
takeUniquePtr(aPtr);

it does not compile. The error I see is

testcpp/hello_world.cpp:141:21: error: invalid user-defined conversion from ‘std::unique_ptr<AClass>’ to ‘const std::unique_ptr<const AClass>&’ [-fpermissive]

Shouldn't the conversion from unique_ptr<AClass> to unique_ptr<AClass const> be automatic? What am I missing here?

By the way, if I change unique_ptr<AClass const> const & aPtr to unique_ptr<AClass> const & aPtr in the function definition, it compiles, but then I can call functions like aPtr->changeAClass(), which I don't want to allow.

9
  • 6
    Why not pass a const reference, then? (instead of a unique_ptr) Commented May 7, 2013 at 17:50
  • 3
    In that case, I would just pass a const AClass *. No need for the callee to know that it's in a unique_ptr. Why couple that tightly? Commented May 7, 2013 at 18:04
  • 2
    @ChrisDodd: notice he passes a reference to unique_ptr, so the object will live on when the helper is done. Commented May 7, 2013 at 18:21
  • 1
    @MarshallClow: But another question remains: Why is compiler not able to cast from unique_ptr<A> to unique_ptr<A const>? (Implicit) non-const to const conversion happens all the time! :-) Commented May 7, 2013 at 18:33
  • 1
    Yes, it does - T and const T but are very different from class<T> and class<const T>. In the first case, the two classes are the same, one just is marked const (there's even a set of template metafunctions for conversion add_const and remove_const. In the second case, there's no way the compiler can be sure that the classes are in any way related (except by name). I can specialize a template for const types and have it be completely different than for non-const types. Commented May 7, 2013 at 19:51

2 Answers 2

45

Smart pointers are for managing ownership and lifetime, they allow us (amongst other things) to safely transfer ownership around the various parts of our code.

When you pass a const unique_ptr<T>& to a function (irrelevant of whether T is const or not), what it actually means is that the function promises to never modify the unique_ptr itself (but it could still modify the pointed-to object if T is not const) ie. there will be no possible transfer of ownership whatsoever. You're just using the unique_ptr as a useless wrapper around a naked pointer.

So, as @MarshallClow suggested in a comment, you should just get rid of the wrapper and pass either a naked pointer or a direct reference. What's cool with this is that your code is now semantically clear (your function's signature clearly states that it does not mess with ownership, which was not immediately obvious with a const unique_ptr<...>&) and it solves your "constification" problem at the same time!

Namely:

void someFunction(const AClass* p) { ... }

std::unique_ptr<AClass> ptr(new AClass());
someFunction(ptr.get());

Edit: to address your secondary question "why won't the compiler let me ... cast unique_ptr<A> to unique_ptr<A const>?".

Actually, you can move a unique_ptr<A> to a unique_ptr<A const>:

std::unique_ptr<A> p(new A());
std::unique_ptr<const A> q(std::move(p));

But as you can see this means a transfer of ownership from p to q.

The problem with your code is that you're passing a (reference to) unique_ptr<const A> to a function. Since there is a type discrepancy with unique_ptr<A>, to make it work the compiler needs to instantiate a temporary. But unless you transfer ownership manually by using std::move, the compiler will try to copy your unique_ptr and it can't do that since unique_ptr explicitly forbids it.

Notice how the problem goes away if you move the unique_ptr:

void test(const std::unique_ptr<const int>& p) { ... }

std::unique_ptr<int> p(new int(3));
test(std::move(p));

The compiler is now able to construct a temporary unique_ptr<const A> and move the original unique_ptr<A> without breaking your expectations (since it is now clear that you want to move, not copy).

So, the root of the problem is that unique_ptr only has move semantics not copy semantics, but you'd need the copy semantics to create a temporary and yet keep ownership afterwards. Egg and chicken, unique_ptr just isn't designed that way.

If you now consider shared_ptr which has copy semantics, the problem also disappears.

void test(const std::shared_ptr<const int>& p) { ... }

std::shared_ptr<int> p(new int(3));
test(p);
//^^^^^ Works!

The reason is that the compiler is now able to create a temporary std::shared_ptr<const int> copy (automatically casting from std::shared_ptr<int>) and bind that temporary to a const reference.

I guess this more or less covers it, even though my explanation lacks standardese lingo and is perhaps not as clear as it should be. :)

Sign up to request clarification or add additional context in comments.

5 Comments

In the second paragraph, as you mentioned, the function "promises" to never modify the object. I guess the question is why won't the compiler let me make a guarantee around that, by allowing me to cast unique_ptr<A> to unique_ptr<A const>? I am guessing I am missing something here. Most of the time, C++ disallows things for a good reason, no? :-)
What I meant by object was the unique_ptr itself, not necessarily the pointed-to object. I realize this does not address the question in your comment, but I thought I'd still clarify (and I will edit my question accordingly). As to the question, well, I agree that "intuitively" one should be able to cast from unique_ptr<A> to unique_ptr<const A> (but obviously not the other way around). But as you say, C++ probably has a good reason for that, we just have to find it. ;)
@YogeshwerSharma: I edited my answer to address the issue. TLDR; you can't create a temporary copy of a unique_ptr because it only has move semantics.
syam: That makes sense. Thanks for taking the time explain this. It is quite clear. The only missing piece in my mind is whether there is really an instantiation of a temporary happening here? I printed something in the constructor of the class, and then passed shared_ptr<A> to a function accepting shared_ptr<A const> and seems like there is no constructor being called. Is this consistent with your understanding?
@YogeshwerSharma: "I printed something in the constructor of the class" => if you mean your pointed-to class, then yes it's consistent: the temporary is a shared_ptr object, the pointed-to object stays the same for both original shared_ptr<A> and temporary shared_ptr<const A>. To observe the temporary you'd have to hack the source of shared_ptr and add a trace in its constructors.
4

Got to this old question on const smart pointers. Above answers ignore the simple template solution.

The very simple template option (option a)

template<class T>
void foo(const unique_ptr<T>& ptr) {
    // do something with ptr
}

this solution allows all possible options of unique_ptr to be sent to foo:

  1. const unique_ptr<const int>
  2. unique_ptr<const int>
  3. const unique_ptr<int>
  4. unique_ptr<int>

If you specifically want to avoid for some reason 3 and 4 above, add const to T:

Accept only const/non-const unique_ptr to const! (option b)

template<class T>
void foo(const unique_ptr<const T>& ptr) {
    // do something with ptr
}

Side note 1

You may overload "option a" and "option b" if to get different behavior for the cases where you can or cannot alter the pointed value.

Side note 2

If you do not want to make any changes to the pointed value in this function (never! whichever type of parameter we get!) -- do not overload.
With "option b", compiler won't allow to change the value we point at. Job done!
If you want to support all 4 cases, i.e. "option a", the function may still "accidentally" change the value we point to, e.g.

template<class T>
void foo(const unique_ptr<T>& ptr) {
    *ptr = 3;
}

however this should not be an issue if at least one caller has T that is actually const, the compiler will not like it in that case and help you get to the problem.
You may add such a caller in unit-test, something like:

    foo(make_unique<const int>(7)); // if this line doesn't compile someone
                                    // is changing value inside foo which is
                                    // not allowed!
                                    // do not delete the test, fix foo!

Code snippet: http://coliru.stacked-crooked.com/a/a36795cdf305d4c7

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.