Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

range based for loop with const shared_ptr<>

I have a container with shared_ptr<>, e.g. a vector<shared_ptr<string>> v and I'd like to iterate over v indicating const-ness.

This code:

vector<shared_ptr<string>> v;
v.push_back(make_shared<std::string>("hallo"));
...

for (const auto &s : v) {
    *s += ".";   // <<== should be invalid
}

looks like what I want to do (indicating that s is const) but of course it does not make the string const.

Is there an elegant way to iterate over a container of shared_ptr which makes clear that the content won't be modified?

Something like

for (shared_ptr<const string> s : v) {
    *s += ".";   // <<== will not compile
}

(but this code would not compile for other reasons :))

Edit:

I made a mistake. Originally I was declaring a reference, which results in a compiler error

for (shared_ptr<const string> &s : v) {   // <<== does not compile
    ...
}

If you declare a shared_ptr<const string> the example works. In my eyes this is a good trade-off but this way the pointer gets copied which can be time consuming in loops with little code and big containers..

like image 248
frans Avatar asked Dec 29 '15 17:12

frans


People also ask

What is a range-based for loop?

The range-based for loop follows this general format: Here’s an simple example which prints all the elements in a std::vector: The range expression typically corresponds to an array or a container object (requiring iterator support). You are also free to use initializer lists as an expression.

How do you exchange a for loop with a shared_ptr?

Include the following code somewhere in your source code before the for loop: The source code above works for shared pointers only, but you can simply exchange std::shared_ptr by std::auto_ptr, std::weak_ptr or whatever type you’d like, assuming it can be dereferenced using *.

What is ranged for loop in C++11?

C++11 introduced the ranged for loop. This for loop is specifically used with collections such as arrays and vectors. Here, the ranged for loop iterates the array num from beginning to end. The int variable var stores the value of the array element in each iteration.

Which statement terminates the current iteration of a range-based for loop?

A continue statement in a range-based for loop terminates only the current iteration. Automatically recognizes arrays. Recognizes containers that have .begin () and .end ().


2 Answers

This is a well-known limitation of C++ that some don't consider to be a limitation.

You want to iterate constly, but an immutable pointer doesn't imply an immutable pointee.

The type shared_ptr<string> and the type shared_ptr<const string> are effectively unrelated.

Option 1

for (const auto& ptr : v) {
    const auto& s = *ptr;

    s += ".";   // <<== is invalid
}

Option 2

Just don't modify it.

like image 152
Lightness Races in Orbit Avatar answered Oct 21 '22 00:10

Lightness Races in Orbit


Here is the answer.

But first, the sermon:

A pointer and the thing it points to are two separate objects. Either, none or both may be const and a const pointer simply means that it will not point to a different thing. If the pointee is const, the object may not be changed through the (possibly non-const) pointer.

Having said that, we (I) often write value-semantic wrapper objects that use unique_ptr or shared_ptr as the pimpl. Often we wish to propogate the constness of the wrapper to impl.

I believe c++17 will solve this with it's propagate_const pointer wrapper.

In the meantime it's straightforward to build your own:

#include <iostream>
#include <type_traits>
#include <memory>
#include <string>
#include <vector>

namespace traits
{
    template<class T> struct pointee;
    template<class T, class D>
    struct pointee<std::unique_ptr<T, D>> {
        using type = T;
    };

    template<class T>
    struct pointee<std::shared_ptr<T>> {
        using type = T;
    };

    template<class T> using pointee_t = typename pointee<T>::type;
}

template<class PointerType>
struct propagate_const
{
    using pointer_type = PointerType;
    using element_type = traits::pointee_t<pointer_type>;
    using value_type = std::decay_t<element_type>;
    using reference = value_type&;
    using const_reference = const value_type&;

    propagate_const(pointer_type p) : _ptr(std::move(p)) {}

    const_reference operator*() const {
        return *_ptr;
    }

    auto operator*()
    -> std::enable_if_t<not std::is_const<element_type>::value, reference>
    {
        return *_ptr;
    }

private:
    pointer_type _ptr;
};

template<class PointerType>
auto make_propagating_pointer(PointerType&& p)
{
    return propagate_const<PointerType>(std::forward<PointerType>(p));
}

int main()
{
    using namespace std;

    vector<propagate_const<shared_ptr<string>>> v;
    v.emplace_back(make_shared<string>("hello"));

    for (const auto& p : v)
    {
//        *p += " there";  // compile error
        cout << *p;
        cout << endl;
    }

    for (auto& p : v)
    {
        *p += " there";
        cout << *p;
        cout << endl;
    }

    return 0;
}

expected output:

hello
hello there

This one is very simple, supporting only operator* but it's trivial to add a complete set of operators. Note that I disable mutable access when the pointee is const.

reference: http://en.cppreference.com/w/cpp/experimental/propagate_const

And just for fun, here's a complete example of a shared_string class that uses shared_ptr internally and propagates constness correctly.

#include <iostream>
#include <type_traits>
#include <memory>
#include <string>
#include <vector>

template<class PointerType>
struct propagate_const
{
    using pointer_type = PointerType;
    using element_type = std::remove_reference_t<decltype(*std::declval<PointerType&>())>;
    using reference = element_type&;
    using const_reference = const element_type&;

    propagate_const(pointer_type p) : _ptr(std::move(p)) {}

    const_reference operator*() const {
        return *_ptr;
    }

    auto operator*()
    -> std::enable_if_t<not std::is_const<element_type>::value, reference>
    {
        return *_ptr;
    }

private:
    pointer_type _ptr;
};

template<class PointerType>
auto make_propagating_pointer(PointerType&& p)
{
    return propagate_const<PointerType>(std::forward<PointerType>(p));
}

struct shared_string
{
    shared_string(std::string s) : _impl(std::make_shared<std::string>(std::move(s))) {};
    shared_string(std::shared_ptr<std::string> sp) : _impl(sp) {};
    shared_string(propagate_const<std::shared_ptr<std::string>> sp) : _impl(sp) {};

    auto& operator += (const std::string& s) {
        *_impl += s;
        return *this;
    }

    friend std::ostream& operator<<(std::ostream& os, const shared_string& ss) {
        return os << *(ss._impl);
    }

private:
    propagate_const<std::shared_ptr<std::string>> _impl;
};

template<class T, std::enable_if_t<std::is_const<T>::value>* = nullptr >
std::string check_const(T&)
{
    return std::string("const");
}

template<class T, std::enable_if_t<not std::is_const<T>::value>* = nullptr >
std::string check_const(T&)
{
    return std::string("not const");
}

int main()
{
    using namespace std;

    // a vector of mutable shared_strings
    vector<shared_string> v;

    // a vector of immutable shared_strings
    vector<const shared_string> cv;

    // make a shared_string
    v.emplace_back(make_shared<string>("hello"));

    // refer to the *same one* in cv
    cv.emplace_back(v[0]);

    for (const auto& p : v)
    {
//        *p += " there";  // immutable reference to mutable shared string - not allowed
        cout << check_const(p) << " " << p;
        cout << endl;
    }

    for (auto& p : v)
    {
        cout << check_const(p) << " " << p;
        p += " there";    // mutable reference to mutable shared string - allowed
        cout << " becomes " << p;
        cout << endl;
    }

    for (auto&p : cv)
    {
        cout << check_const(p) << " " << p;
//        p += " world";     // p is actually immutable because cv contains immutable objects
        cout << endl;
    }

    return 0;
}

expected output:

const hello
not const hello becomes hello there
const hello there
like image 32
Richard Hodges Avatar answered Oct 21 '22 00:10

Richard Hodges