简体   繁体   中英

Passing a std::shared_ptr<T> to a function that takes a std::shared_ptr<const T>?

I have a function that needs to take shared ownership of an argument, but does not modify it. I have made the argument a shared_ptr<const T> to clearly convey this intent.

template <typename T>
void func(std::shared_ptr<const T> ptr){}

I would like to call this function with a shared_ptr to a non-const T. For example:

auto nonConstInt = std::make_shared<int>();
func(nonConstInt);

However this generates a compile error on VC 2017:

error C2672: 'func': no matching overloaded function found
error C2784: 'void func(std::shared_ptr<const _Ty>)': could not deduce template argument for 'std::shared_ptr<const _Ty>' from 'std::shared_ptr<int>'
note: see declaration of 'func'

Is there a way to make this work without:

  • Modifying the calls to func. This is part of a larger code refactoring, and I would prefer not to have to use std::const_pointer_cast at every call site.
  • Defining multiple overloads of func as that seems redundant.

We are currently compiling against the C++14 standard, with plans to move to c++17 soon, if that helps.

template <typename T>
void cfunc(std::shared_ptr<const T> ptr){
  // implementation
}
template <typename T>
void func(std::shared_ptr<T> ptr){ return cfunc<T>(std::move(ptr)); }
template <typename T>
void func(std::shared_ptr<const T> ptr){ return cfunc<T>(std::move(ptr)); }

this matches how cbegin works, and the "overloads" are trivial forwarders with nearly zero cost.

Unfortunately, there is no good solution to what you desire. The error occurs because it fails to deduce template argument T . During argument deduction it attempts only a few simple conversations and you cannot influence it in any way.

Think of it: to cast from std::shared_ptr<T> to some std::shared_ptr<const U> it requires to know U , so how should compiler be able to tell that U=T and not some other type? You can always cast to std::shared_ptr<const void> , so why not U=void ? So such searches aren't performed at all as in general it is not solvable. Perhaps, hypothetically one could propose a feature where certain user-explicitly-declared casts are attempted for argument deduction but it isn't a part of C++.

Only advise is to write function declaration without const :

    template <typename T>
    void func(std::shared_ptr<T> ptr){}

You could try to show your intent by making the function into a redirection like:

    template <typename T>
    void func(std::shared_ptr<T> ptr)
    {
           func_impl<T>(std::move(ptr));
    }

Where func_impl is the implementation function that accepts a std::shared_ptr<const T> . Or even perform const cast directly upon calling func_impl .

Thanks for the replies.

I ended up solving this a slightly different way. I changed the function parameter to just a shared_ptr to any T so that it would allow const types, then I used std::enable_if to restrict the template to types that I care about. (In my case vector<T> and const vector<T> )

The call sites don't need to be modified. The function will compile when called with both shared_ptr<const T> and shared_ptr<T> without needing separate overloads.

Here's a complete example that compiles on VC, GCC, and clang:

#include <iostream>
#include <memory>
#include <vector>

template<typename T>
struct is_vector : public std::false_type{};

template<typename T>
struct is_vector<std::vector<T>> : public std::true_type{};

template<typename T>
struct is_vector<const std::vector<T>> : public std::true_type{};

template <typename ArrayType,
         typename std::enable_if_t<is_vector<ArrayType>::value>* = nullptr>
void func( std::shared_ptr<ArrayType> ptr) {
}

int main()
{
    std::shared_ptr< const std::vector<int> > constPtr;
    std::shared_ptr< std::vector<int> > nonConstPtr;
    func(constPtr);
    func(nonConstPtr);
}

The only downside is that the non-const instantiation of func will allow non-const methods to be called on the passed-in ptr. In my case a compile error will still be generated since there are some calls to the const version of func and both versions come from the same template.

As the const is only for documentation, make it a comment:

template <class T>
void func(std::shared_ptr</*const*/ T> p) {
}

You could additionally delegate to the version getting a pointer to constant object if the function is hefty enough to make it worthwhile:

template <class T>
void func(std::shared_ptr</*const*/ T> p) {
    if (!std::is_const<T>::value) // TODO: constexpr
        return func<const T>(std::move(p));
}

No guarantee the compiler will eliminate the move though.

You certainly don't want to be modifying the call sites, but you sure can be modifying the functions themselves - that's what you implied in the question anyway. Something had to be changed somewhere , after all.

Thus:

In C++17 you could use deduction guides and modify call sites, but in a less intrusive manner than with a cast. I'm sure a language lawyer can pitch in about whether adding a deduction guide to the std namespace is allowed. At the very least we can limit the applicability of those deduction guides to the types we care about - it won't work for the others. That's to limit potential for mayhem.

template <typename T>
class allow_const_shared_ptr_cast : public std::integral_constant<bool, false> {};
template <typename T>
static constexpr bool allow_const_shared_ptr_cast_v = allow_const_shared_ptr_cast<T>::value;

template<>
class allow_const_shared_ptr_cast<int> : public std::integral_constant<bool, true> {};

template <typename T>
void func(std::shared_ptr<const T> ptr) {}

namespace std {
template<class Y> shared_ptr(const shared_ptr<Y>&) noexcept 
    -> shared_ptr<std::enable_if_t<allow_const_shared_ptr_cast_v<Y>, const Y>>;
template<class Y> shared_ptr(shared_ptr<Y>&&) noexcept
    -> shared_ptr<std::enable_if_t<allow_const_shared_ptr_cast_v<Y>, const Y>>;
}

void test() {
    std::shared_ptr<int> nonConstInt;
    func(std::shared_ptr(nonConstInt));
    func(std::shared_ptr(std::make_shared<int>()));
}

std::shared_ptr is certainly less wordy than std::const_pointer_cast<SomeType> .

This should not have any performance impact, but sure modifying the call sites is a pain.

Otherwise there's no solution that doesn't involve modifying the called function declarations - but the modification should be acceptable, I think, since it's not any more wordy than what you had already:

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM