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..
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.
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 *.
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.
A continue statement in a range-based for loop terminates only the current iteration. Automatically recognizes arrays. Recognizes containers that have .begin () and .end ().
This is a well-known limitation of C++ that some don't consider to be a limitation.
You want to iterate const
ly, 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.
for (const auto& ptr : v) {
const auto& s = *ptr;
s += "."; // <<== is invalid
}
Just don't modify it.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With