13

Let

class A
{
    std::vector<std::shared_ptr<int>> v_;
};

Now I'd like to add access to v_ using two public member functions

std::vector<std::shared_ptr<int>> const & v() { return v_; }

and

std::vector<std::shared_ptr<int const> const & v() const { TODO }

I cannot replace TODO with return v_; though.

One option would be to not return a reference but a copy. Apart from the obvious performance penalty, this would also make the interface somewhat less desirable.

Another option is to make TODO equal to return reinterpret_cast<std::vector<std::shared_ptr<int const>> const &>(v_);

My question is, is this undefined behavior? Or, alternatively, is there a better option, preferably without using reinterpret_cast?

Xoph
  • 459
  • 2
  • 10
  • 1
    Unrelated to your problem, but why do you have a pointer to an `int` (or `const int`)? If you have a pointer to an "array" wouldn't a vector make more sense? – Some programmer dude Feb 06 '18 at 11:24
  • 1
    You can return an iterator – Yola Feb 06 '18 at 11:30
  • 1
    The `int` was merely an example, in the actual code it's a big class. – Xoph Feb 06 '18 at 11:33
  • Regarding the "performance penalty" have you *measured* it? Modern compilers are pretty good at optimizing such situations, especially when it can employ move semantics. – Some programmer dude Feb 06 '18 at 11:47
  • What are you hoping to accomplish by allowing direct access to `v_`? I'm asking because I believe that will dictate what's the best answer – StoryTeller - Unslander Monica Feb 06 '18 at 11:55
  • 1
    @Someprogrammerdude I'm actually not concerned about the performance. I would be fine with returning a copy. But then I'd better also return a copy in the non-const case to have a uniform interface. This in turn makes it impossible to return a `&`, not a `const &` in the non-const case. – Xoph Feb 06 '18 at 11:56
  • @StoryTeller I'd like to, for example, allow range-based loops over `v_`. In my actual code, I'd prefer to return a `std::vector> &` with non-const `v()` so that an outside user can even change `v_`. – Xoph Feb 06 '18 at 11:58
  • You shouldn't be returning a vector at all if the caller doesn't need the vector interface. You can probably do what you want by defining your own proxy type that offers `begin`, `end` and `operator[]`. For the `const` case I don't think you'll avoid returning a copy of the `shared_ptr` as it doesn't seem like you can safely `reinterpret_cast` it to `shared_ptr` without relying on specific implementations. But at least you won't have to preemptively copy the whole vector. – patatahooligan Feb 06 '18 at 12:21

2 Answers2

6

A way to avoid copying the container is to provide transform iterators that transform the element on dereference:

#include <vector>
#include <memory>
#include <boost/iterator/transform_iterator.hpp>

class A
{
    std::vector<std::shared_ptr<int> > v_;

    struct Transform
    {
        template<class T>
        std::shared_ptr<T const> operator()(std::shared_ptr<T> const& p) const {
            return p;
        }
    };

public:

    A() : v_{std::make_shared<int>(1), std::make_shared<int>(2)} {}

    using Iterator = boost::transform_iterator<Transform, std::vector<std::shared_ptr<int> >::const_iterator>;

    Iterator begin() const { return Iterator{v_.begin()}; }
    Iterator end() const { return Iterator{v_.end()}; }

};

int main() {
    A a;
    // Range access.
    for(auto const& x : a)
        std::cout << *x << '\n';
    // Indexed access.
    auto iterator_to_second_element = a.begin() + 1;
    std::cout << **iterator_to_second_element << '\n';
}
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • Using `boost::transform_iterator` is an elegant way to do the "proxy approach". Thank you for the good suggestion! – Xoph Feb 06 '18 at 12:43
3

Putting aside the discussion of whether or not you should return a reference to a member...

std::vector already propagates its own const qualifier to the references, pointee's and iterators it returns. The only hurdle is making it propagate further to the pointee type of the std::shared_ptr. You can use a class like std::experimental::propagate_const (that will hopefully be standardized) to facilitate that. It will do as its name implies, for any pointer or pointer-like object it wraps.

class A
{
    using ptr_type = std::experimental::propagate_const<std::shared_ptr<int>>;
    std::vector<ptr_type> v_;
};

Thus TODO can become return v_;, and any access to the pointees (like in the range-based for you wish to support) will preserve const-ness.

Only caveat is that it's a moveable only type, so copying out an element of the vector will require a bit more work (for instance, by calling std::experimental::get_underlying) with the element type of the vector itself.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Thanks for the suggestions. I actually tried using `propagate_const` first. However, the caveat you mention seems rather strong to me. There is a member function `get()` in `propagate_const` that does nicely what I'd like, but only for raw pointers. There is no `get_underlying()` member function that returns the actual pointer-like type held in a const-correct way. But using the non-member `get_underlying()` undermines the whole const-correctness. – Xoph Feb 06 '18 at 12:42
  • @Xoph I don't follow. Member vs non-member doesn't matter here. You would either have an implicit `const propagate_const *` (`this`) or an explicit `const propagate_const &` (parameter), with the same const semantics – Caleth Feb 06 '18 at 13:01
  • @Caleth What I'm trying to say is: if you have a `const propagate_cast>`, getting a `shared_ptr` from it is harder than I'd like it to be and involves breaking const-correctness by requiring you to use `get_underlying()` to get a `shared_ptr` and then converting it into a `shared_ptr` from what I can tell. – Xoph Feb 06 '18 at 13:12
  • @Xoph Yes, because `shared_ptr` is a different type to `shared_ptr` (although there is a converting constructor which maintains the shared-ness) – Caleth Feb 06 '18 at 13:25
  • @Xoph [`const`] `propagate_const>` will do what you want except for being non-copyable. If you can define the API yourself, and don't need copies, there is no reason to use `shared_ptr`. So this a big question here: Do you need copies? Then again, if you *don't* need copies, what is the point of using `shared_ptr` other than more concise code? – Arne Vogel Feb 06 '18 at 14:10
  • @ArneVogel I do need copies. :) – Xoph Feb 06 '18 at 16:08