Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 439
Indigox3 Avatar asked Nov 26 '20 17:11

Indigox3


3 Answers

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.

like image 68
Yakk - Adam Nevraumont Avatar answered Oct 25 '22 20:10

Yakk - Adam Nevraumont


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.

like image 28
ALX23z Avatar answered Oct 25 '22 21:10

ALX23z


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.

like image 44
Indigox3 Avatar answered Oct 25 '22 20:10

Indigox3