How to return a private pointer to a list of pointers as const?

387 views Asked by At

I have a pointer to a list of pointers, as a private variable. I also have a getter that returns the pointer to the list. I need to protect it from changes.

I couldn't find how to use reinterpret_cast or const_cast on this.

class typeA{
    shared_ptr<list<shared_ptr<typeB>>> l;
public:
   shared_ptr<list<shared_ptr<const typeB>>> getList(){return (l);};
};

The compiler returns:

   error: could not convert ‘((typeA*)this)->typeA::x’ from ‘std::shared_ptr<std::__cxx11::list<std::shared_ptr<typeB> > >’ to ‘std::shared_ptr<std::__cxx11::list<std::shared_ptr<const typeB> > >’|
||=== Build failed: 1 error(s), 0 warning(s) (0 minute(s), 0 second(s)) ===|

It seems as const shared_ptr<list<shared_ptr<typeB>>> and shared_ptr<const list<shared_ptr<typeB>>> work fine.

Is it possible to do return l as a complete const, like:

const shared_ptr<const list<shared_ptr<const typeB>>>

or at least like:

shared_ptr<list<shared_ptr<const typeB>>> 

?

References instead of pointers is not an option. To declare l as shared_ptr<list<shared_ptr<const typeB>>> also is not a wanted solution.

EDIT: no 'int' anymore.

It seems as it is not possible exactly what I wanted, but the suggested solutions are good. Yes, copying pointers is acceptable.

My bad i didn't put typeB immediately. I am aware of some advantages of references over pointers, but I hoped there is some similar solution.

5

There are 5 answers

7
Ted Lyngmo On BEST ANSWER

You can create a new list of const int's from your original list and return that:

std::shared_ptr<std::list<std::shared_ptr<const int>>> getList(){
    return std::make_shared<std::list<std::shared_ptr<const int>>>(l->begin(), l->end());
}

If you want to prevent people from making changes to the returned list, make it const too:

std::shared_ptr<const std::list<std::shared_ptr<const T>>> getList(){
    return std::make_shared<const std::list<std::shared_ptr<const T>>>(l->cbegin(), l->cend());
}

The shared pointer returned by this function does not point to the original list but to the newly created list.

An alternative may be to provide iterators that, when dereferenced, returns const T& (where T is the type you actually store). That way there will be no need to copy the whole list every time you want to go though it. Example:

#include <iostream>
#include <list>
#include <memory>

struct example {
    int data;
    example(int x) : data(x) {}
};

template <class T>
class typeA {
    std::shared_ptr<std::list<std::shared_ptr<T>>> l = std::make_shared<std::list<std::shared_ptr<T>>>();
public:
    template< class... Args >
    void add( Args&&... args ) {
        l->emplace_back(std::make_shared<T>(std::forward<Args>(args)...));
    }

    // a very basic iterator that can be extended as needed   
    struct const_iterator {
        using uiterator = typename std::list<std::shared_ptr<T>>::const_iterator;
        uiterator lit;
        const_iterator(uiterator init) : lit(init) {}
        const_iterator& operator++() { ++lit; return *this; }
        const T& operator*() const { return *(*lit).get(); }
        bool operator!=(const const_iterator& rhs) const { return lit != rhs.lit; }
    };

    const_iterator cbegin() const noexcept { return const_iterator(l->cbegin()); }
    const_iterator cend() const noexcept { return const_iterator(l->cend()); }
    auto begin() const noexcept { return cbegin(); }
    auto end() const noexcept { return cend(); }
};

int main() {
    typeA<example> apa;
    apa.add(10);
    apa.add(20);
    apa.add(30);
    for(auto& a : apa) {
        // a.data = 5; // error: assignment of member ‘example::data’ in read-only object
        std::cout << a.data << "\n";
    }
}
0
Aconcagua On

The problem with templates is that for any

template <typename T>
class C { };

any two pairs C<TypeA> and C<TypeB> are totally unrelated classes – this is even the case if TypeA and TypeB only differ in const-ness.

So what you actually want to have is technically not possible. I won't present a new workaround for now, as there are already, but try to look a bit further: As denoted in comments already, you might be facing a XY problem.

Question is: What would a user do with such a list? She/he might be iterating over it – or access single elements. Then why not make your entire class look/behave like a list?

class typeA
{
    // wondering pretty much why you need a shared pointer here at all!
    // (instead of directly aggregating the list)
    shared_ptr<list<shared_ptr<typeB>>> l;
public:
    shared_ptr<list<shared_ptr<typeB>>>::const_iterator begin() { return l->begin(); }
    shared_ptr<list<shared_ptr<typeB>>>::const_iterator end() { return l->end(); }
};

If you used a vector instead of a list, I'd yet provide an index operator:

shared_ptr<typeB /* const or not? */> operator[](size_t index);

Now one problem yet remains unsolved so far: The two const_iterators returned have an immutable shared pointer, but the pointee is still mutable!

This is a bit of trouble - you'll need to implement your own iterator class now:

class TypeA
{
public:
    class iterator
    {
        std::list<std::shared_ptr<int>>::iterator i;         
        public:
        // implementation as needed: operators, type traits, etc.
    };
};

Have a look at std::iterator for a full example – be aware, though, that std::iterator is deprecated, so you'll need to implement the type-traits yourself.

The iterator tag to be used would be std::bidirectional_iterator_tag or random_access_iterator_tag (contiguous_iterator_tag with C++20), if you use a std::vector inside.

Now important is how you implement two of the needed operators:

std::shared_ptr<int const> TypeA::iterator::operator*()
{
    return std::shared_ptr<int const>(*i);
}

std::shared_ptr<int const> TypeA::iterator::operator->()
{
    return *this;
}

The other operators would just forward the operation to the internal iterators (increment, decrement if available, comparison, etc).

I do not claim this is the Holy Grail, the path you need to follow under all circumstances. But it is a valuable alternative worth to at least consider...

1
Ap31 On

What you have here is a VERY complex construct:

shared_ptr<list<shared_ptr<typeB>>> l;

This is three levels of indirection, of which two have reference counting lifetime management, and the third is a container (and not memory-contiguous at that).

Naturally, given this complex structure, it is not going to be easy to convert it to another type:

shared_ptr<list<shared_ptr<const typeB>>>

Notice that std::list<A> and std::list<const A> are two distinct types by design of standard library. When you want to pass around non-modifying handles to your containers, you are usually supposed to use const_iterators.
In your case there is a shared_ptr on top of the list, so you can't use iterators if you want that reference counting behavior.

At this point comes the question: do you REALLY want that behavior?

  • Are you expecting a situation where your typeA instance is destroyed, but you still have some other typeA instances with the same container?
  • Are you expecting a situation where all your typeA instances sharing the container are destroyed, but you still have some references to that container in other places of your runtime?
  • Are you expecting a situation where the container itself is destroyed, but you still have some references to some of the elements?
  • Do you have any reason at all to use std::list instead of more conventional containers to store shared pointers?

If you answer YES to all the bullet points, then to achieve your goal you'll probably have to design a new class that would behave as a holder for your shared_ptr<list<shared_ptr<typeB>>>, while only providing const access to the elements.

If, however, on one of the bullet points your answer is NO, consider redesigning the l type. I suggest starting with std::vector<typeB> and then only adding necessary modifications one by one.

2
eerorika On

When you convert a pointer-to-nonconst to a pointer-to-const, you have two pointers. Furthermore, a list of pointers-to-nonconst is a completely different type from a list of pointers-to-const.

Thus, if you want to return a pointer to a list of pointers-to-const, what you must have is a list of pointers-to-const. But you don't have such list. You have a list of pointers-to-nonconst and those list types are not interconvertible.

Of course, you could transform your pointers-to-nonconst into a list of pointers-to-const, but you must understand that it is a separate list. A pointer to former type cannot point to the latter.

So, here is an example to transform the list (I didn't test, may contain typos or mistakes):

list<shared_ptr<const int>> const_copy_of_list;
std::transform(l->begin(), l->end(), std::back_inserter(const_copy_of_list),
               [](auto& ptr) {
    return static_pointer_cast<const int>(ptr);
});
// or more simply as shown by Ted:
list<shared_ptr<const int>> const_copy_of_list(l->begin(), l->end());

Since we have created a completely new list, which cannot be pointed by l, it makes little sense to return a pointer. Let us return the list itself. The caller can wrap the list in shared ownership if the need it, but don't have to when it is against their needs:

 list<shared_ptr<const int>> getConstCopyList() {
      // ... the transorm above
      return const_copy_of_list;
 }

Note that while the list is separate, the pointers inside still point to the same integers.


As a side note, please consider whether shared ownership of an int object makes sense for your program - I'm assuming it is a simplification for the example.

Also reconsider whether "References instead of pointers is not an option" is a sensible requirement.

4
Caleth On

You problem squarely lies at

but I do not want to mix references and pointers. It is easier and cleaner to have just pointers.

What you are finding here is that statement is wrong. A list<TypeB> can bind a const list<TypeB> & reference, and none of the list's members will allow any modification of the TypeB objects.

class typeA {
    std::vector<typeB> l;
public:
    const std::vector<typeB> & getList() const { return l; };
};

If you really really must have const typeB, you could instead return a projection of l that has added const, but that wouldn't be a Container, but instead a Range (using the ranges library voted into C++20, see also its standalone implementation)

std::shared_ptr<const typeB> add_const(std::shared_ptr<typeB> ptr)
{
    return { ptr, ptr.get() };
}

class typeA {
    std::vector<std::shared_ptr<typeB>> l;
public:
    auto getList() const { return l | std::ranges::transform(add_const); };
};

Another alternative is that you can wrap your std::shared_ptrs in something like std::experimental::propagate_const, and just directly return them.