10

I want to apply the same template algorithm to std::vectors which contain objects of some type T and (different) std::vectors which contain std::shared_ptrs to objects of some type T.

Can I distinguish these types in the template so that I could dereference the pointer when the object in the std::vector is a std::shared_ptr and don't do so if the type is not a std::shared_ptr ?

Here is the code:

#include <vector>
#include <memory>

struct S {
    void member() const {}
};

void fn(const auto& arr) {
    for (const auto& val : arr) {
        // This won't compile with call fn(objects) below
        const S& obj = *val;

        // I want to have something like (I understand that I mix different things, I just want to show the idea)
        const auto& obj = std::is_same<val, std::shared_ptr> ? (*val) : val;
        // Or even better
        const S& obj = std::is_same<val, std::shared_ptr> ? (*val) : val;

        obj.member();
    }
}

int main()
{
    std::vector<S> objects;
    std::vector<std::shared_ptr<S>> pointers;

    // I want make 'fn' transparent for use containers of types T and std::shared_ptr<T>
    fn(objects);
    fn(pointers);
}

It seems that I can pass "de-wrapper" functor as a second argument to the call and make access over there, but I don't want to overcomplicate the client code.

2
  • Which c++ standard can you use ? Commented Feb 23 at 21:09
  • @edrezen, any recent version would work, line C++20 or C++23. I am using MSVC++ with /std:c++latest compiler option. Commented Feb 23 at 21:15

5 Answers 5

10

You could add a type trait to check if a type is a std::shared_ptr<something>:

#include <type_traits>

template<class T>
struct is_shared_ptr : std::false_type {};

template<class T>
struct is_shared_ptr<std::shared_ptr<T>> : std::true_type {};

template<class T>
inline constexpr bool is_shared_ptr_v = is_shared_ptr<T>::value;

Example usage:

void fn(const auto& arr) {
    for (const auto& val : arr) {
        const S& obj = [&]() -> const S& {
            if constexpr (is_shared_ptr_v<std::remove_cvref_t<decltype(val)>>) {
                return *val;
            } else {
                return val;
            }
        }();

        obj.member();
    }
}
Sign up to request clarification or add additional context in comments.

5 Comments

Is there any reason for the immediately evaluated lambda? A ternary expression would be a bit less cumbersome, no?
@MatthieuM. It would have been if it had been a valid construct, but without if constexpr (or something to that effect), *val and val will both have to be valid no matter if it's a shared pointer or not, and they are not. example
Thank you, worked like a charm! If I want to clean up client code and extract comparison to a separate deref_if_needed like I've done here, would it be correct or I miss some potential issues or possibly unnecessary copies?
@DamirTenishev Glad to hear it! I think your deref_if_needed is fine, but if that's the usage you want, I recommend Jeff Garret's two overloads instead. Edit: Aha, I see your comment to his answer. Great :-)
Yes, you are correct, my wording of the question was wrong, since I didn't know the answer and suggested not the best approach as an example of how I want to get it solved; just wanted to explain better the question and got it wrong. I pointed the same in my comment to the answer by Jeff Garrett. At the same time, at the moment I consider approach by Marek R is the ultimate because it generic (work for all types) and robust. Meanwhile, your answer gives me the most flexibility and control in case of other needs there, so I accepted your answer since it quite specific to the question.
7

I would consider an overload set as the most straightforward option.

#include <vector>
#include <memory>

struct S {
    void member() const {}
};

const auto& deref(const auto& v) { return v; }
template <typename T> const T& deref(const std::shared_ptr<T>& ptr) { return *ptr; }

void fn(const auto& arr) {
    for (const auto& val : arr) {
        const S& obj = deref(val); 
        obj.member();
    }
}

int main()
{
    std::vector<S> objects;
    std::vector<std::shared_ptr<S>> pointers;
    
    // fn is transparent for use containers of types T and std::shared_ptr<T>
    fn(objects);
    fn(pointers);
}

https://godbolt.org/z/TKPefPnKr

1 Comment

Thank you for the input, it really helped and most likely is the best fit for the root cause of the question. Being honest, I've used your solution in my code. I accepted the answer by Ted Lyngmo just because of the perfect match to my question's wording. Not knowing the answer I provided a tricky approach as a possible solution I am looking for, so my question wording was not so good. So, to avoid frustration for further readers I accepted the answer which is closer to initial question wording. And for the same readers I confess here that I would use your solution for the problem itself.
6

If you just want to invoke a member function, then by far the easiest option would be to just use std::invoke():

Compiler Explorer

void fn(const auto& arr) {
    for (const auto& val : arr) {
        std::invoke(&S::member, val);
    }
}

It is easy to read, easy to write and works out of the box for basically all (smart) pointer types.

In this case, std::invoke(&S::member, val) will result in one of those two invocations:

  • (val.*(&S::member))(); (if val is S or a reference to it)
  • (val->*(&S::member))(); (if val is a pointer to S or smart pointer to S) (all standard smart pointers overload operator->*, just like they overload operator->)

1 Comment

Thank you for the input. It helps. Honestly, in the current situation I need access to the members, as well, so I have to use other solutions, but I will remember the solution for future occasions.
3

You can use std::views::transform to adapt the object to be iterated through a function mytransfo; this function would have a specific template specialization for handling std::shared_ptr objects.

Note that for the generic case, you could simply return the array itself, and not returning a view.

#include <vector>
#include <memory>
#include <ranges>

struct S {
    void member() const {}
};

template<typename Array>
auto mytransfo (Array const& arr) 
{
    return arr | std::views::transform([] (auto const& x) { return x; } );
};

template<typename T>
auto mytransfo (std::vector<std::shared_ptr<T>> const& arr) 
{
    return arr | std::views::transform([] (auto const& x) { return *x; } );
};

void fn(const auto& arr) 
{
    for (const auto& obj : mytransfo(arr))  {  obj.member(); }
}

int main()
{
    std::vector<S> objects;
    std::vector<std::shared_ptr<S>> pointers;
    
    fn(objects);
    fn(pointers);
}

DEMO

By doing this, your fn remains simple and you delegate the decision "object/pointer" logic to another part.


Update

According to @Jarod42's remark, one should take care about the return type of the lambda and make sure one returns a reference and not a copy. So we can use a decltype(auto) as a return type here (and not a simple auto that would decay the type and return a copy).

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

struct S 
{
    S()         { std::cout << "default\n"; }
    S(S const&) { std::cout << "copy\n";    }
    S(S     &&) { std::cout << "move\n";    }
    
    void member() const {}
};

template<typename Array>
auto mytransfo (Array const& arr) 
{
    return arr | std::views::transform([] (auto const& x) -> decltype(auto) { return x; } );
};

template<typename T>
auto mytransfo (std::vector<std::shared_ptr<T>> const& arr) 
{
    return arr | std::views::transform([] (auto const& x) -> decltype(auto) { return *x; } );
};

void fn(const auto& arr) 
{
    for (const auto& obj : mytransfo(arr))  {  obj.member(); }
}

int main()
{
    std::vector<S> objects;
    objects.push_back (S{});
    fn(objects);
 
    std::vector<std::shared_ptr<S>> pointers;
    pointers.push_back (std::make_shared<S>(S{}));
    fn(pointers);
}

DEMO

2 Comments

Take care with the lambdas, they currently return copy, and not reference.
Thank you for the solution, a nice one! It is very close to one provided by Jeff Garrett, but piping makes it even better in terms of client code. So, the same comments and thanks I've provided in the comments to Jeff Garrett go to you. Most likely I will rewrite the code to your approach.
2

With use of C++20 concepts, the code is pretty nice:

template <typename T>
concept range_of_pointers = std::ranges::range<T> && requires(T r) {
    **std::begin(r);
};

template <std::ranges::range T>
decltype(auto) pointer_transparent(T&& r)
{
    return std::forward<T>(r);
}

template <range_of_pointers T>
auto pointer_transparent(T&& r)
{
    return std::views::transform(std::forward<T>(r),
        [](const auto& p) -> decltype(auto) { return *p; });
}

Live demo

And if pipe syntax it nice to have, then you can do this:

template <typename T>
concept range_of_pointers = std::ranges::range<T> && requires(T r) {
    **std::begin(r);
};

struct pointer_transparent_fn {
    template <std::ranges::range T>
    constexpr decltype(auto) operator()(T&& r) const
    {
        return std::forward<T>(r);
    }

    template <range_of_pointers T>
    constexpr auto operator()(T&& r) const
    {
        return std::views::transform(std::forward<T>(r),
            [](const auto& p) -> decltype(auto) { return *p; });
    }

    template <typename T>
    constexpr friend decltype(auto) operator|(T&& r, const pointer_transparent_fn& self)
    {
        return self(std::forward<T>(r));
    }
};

inline constexpr pointer_transparent_fn pointer_transparent;

Live demo

1 Comment

Thank you for the solution. I guess this one is a kind of "library solution", I mean this is the most generic and robust code along the answers here. And it comes with an extra benefit; namely, I have to learn all its parts. I can easily read and support this code but still can't be sure if I'll be able to answer to any question like "If you change this to that here, what would happen?". This is the only showstopper for me to use your solution so far. So, most of words and thanks, I provided in my comment to the answer by Jeff Garrett go to you, as well.

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.