10

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.

Indigox3
  • 133
  • 7
  • I think templates are taking higher preference, if this would be fully typed it works. You could add an overload, though that doesn't make a lot of sense. Just to be sure, you need a shared_ptr here instead of a reference? – JVApen Nov 26 '20 at 18:35
  • 1
    Yes, it works if func is not a template (which is why I was surprised it didn't work for templated functions). func is taking shared ownership of the pointed to object, so I can't take a reference to T. – Indigox3 Nov 26 '20 at 18:42
  • Goh, foresee a free method to add the const to the shared_ptr would be my pragmatic solution, or add the 2 overloads and explicitly add the const before redirecting. – JVApen Nov 26 '20 at 18:47

5 Answers5

5
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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Hm. And here I thought `cbegin()` generally delegates to `begin()`. – Deduplicator Nov 27 '20 at 02:48
  • Why not... `std::shared_ptr lp = std::move(ptr);` as the first line in func()? – Loreto Nov 30 '20 at 10:28
  • 1
    Even though [your suggestion works](https://godbolt.org/z/59b3G9occ) it can also be done [with a single delegating function](https://godbolt.org/z/1fqGbEsxY) is there any advantage for the two delegating functions? – Amir Kirsh Dec 18 '22 at 11:26
  • @AmirKirsh No, not that I can think of. Other than seeing the `const` in the signature of `func`, which could help with code assist tools. – Yakk - Adam Nevraumont Dec 18 '22 at 17:53
1

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.

ALX23z
  • 4,456
  • 1
  • 11
  • 18
1

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.

Indigox3
  • 133
  • 7
0

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.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
0

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:

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313