如何将指向指针列表的私有指针作为 const 返回

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

本文关键字:指针 返回 const 列表      更新时间:2023-10-16

我有一个指向指针列表的指针,作为私有变量。我还有一个 getter 返回指向列表的指针。我需要保护它免受更改。

我找不到如何使用reinterpret_cast或const_cast。

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

编译器返回:

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)) ===|

看起来const shared_ptr<list<shared_ptr<typeB>>>shared_ptr<const list<shared_ptr<typeB>>>工作正常。

是否可以将返回l作为完全常量,例如:

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

或者至少喜欢:

shared_ptr<list<shared_ptr<const typeB>>> 

引用而不是指针不是一种选择。将l声明为shared_ptr<list<shared_ptr<const typeB>>>也不是想要的解决方案。

编辑:不再有"int"。

似乎不可能完全满足我的需求,但建议的解决方案很好。是的,复制指针是可以接受的。

我的坏我没有立即输入B型。我知道引用相对于指针的一些优点,但我希望有一些类似的解决方案。

您可以从原始列表中创建一个新的const int列表并返回:

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());
}

如果要阻止用户对返回的列表进行更改,请使其也具有 const:

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());
}

此函数返回的共享指针不指向原始列表,而是指向新创建的列表。

另一种方法是提供迭代器,当取消引用时,返回const T&(其中T是您实际存储的类型)。这样,就无需在每次要浏览列表时都复制整个列表。例:

#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";
}
}

当您将指针到非常量转换为指向常量指针时,您有两个指针。此外,指向非常量的指针列表与指向常量的指针列表是完全不同的类型。

因此,如果要返回指向常量指针列表的指针,则必须具有指向常量指针的列表。但是你没有这样的清单。您有一个指向非常量的指针列表,这些列表类型不可相互转换。

当然,您可以将指向 non-const 的指针转换为指向 const 的指针列表,但您必须了解它是一个单独的列表。指向前一种类型的指针不能指向后者。

所以,这里有一个转换列表的例子(我没有测试,可能包含错别字或错误):

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());

由于我们创建了一个全新的列表,l无法指出,因此返回指针毫无意义。让我们返回列表本身。如果需要,调用方可以将列表包装在共享所有权中,但当它不符合他们的需求时,则不必这样做:

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

请注意,虽然列表是分开的,但内部的指针仍指向相同的整数。


作为旁注,请考虑int对象的共享所有权是否对您的程序有意义 - 我假设这是示例的简化。

还要重新考虑"引用而不是指针不是一种选择"是否是一个明智的要求。

你的问题直接在于

但我不想混合引用和指针。只有指针更容易、更干净。

你在这里发现的是这种说法是错误的list<TypeB>可以绑定const list<TypeB> &引用,并且列表的任何成员都不允许对TypeB对象进行任何修改。

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

如果你真的必须有const typeB,你可以返回一个添加了constl投影,但那将不是一个容器,而是一个范围(使用投票进入C++20的ranges库,另请参阅其独立实现)

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); };
};

另一种选择是您可以将std::shared_ptr包裹在类似std::experimental::propagate_const的东西中,然后直接将它们退回。

你在这里有一个非常复杂的结构:

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

这是三个间接级别,其中两个具有引用计数生存期管理,第三个是容器(而不是内存连续)。

当然,鉴于这种复杂的结构,将其转换为另一种类型并不容易:

shared_ptr<list<shared_ptr<const typeB>>>

请注意,根据标准库的设计,std::list<A>std::list<const A>是两种不同的类型。当您想要将非修改句柄传递给容器时,通常应该使用const_iterators.
在您的情况下,list顶部有一个shared_ptr,因此如果您想要引用计数行为,则不能使用迭代器。

在这一点上,问题来了:你真的想要这种行为吗?

  • 您是否预计会出现typeA实例被销毁,但仍有一些其他typeA实例具有相同的容器的情况?
  • 您是否预计会出现这样一种情况:共享容器的所有typeA实例都被销毁,但在运行时的其他位置仍有对该容器的一些引用?
  • 您是否预计会出现容器本身被破坏的情况,但您仍然对某些元素有一些引用?
  • 您是否有任何理由使用std::list而不是更传统的容器来存储共享指针?

如果你对所有要点的回答都是"是",那么为了实现你的目标,你可能必须设计一个新的类,它的行为可以作为你的shared_ptr<list<shared_ptr<typeB>>>的持有者,同时只提供对元素的const访问。

但是,如果在其中一个要点上,您的答案是否定的,请考虑重新设计l类型。我建议从std::vector<typeB>开始,然后只逐个添加必要的修改。

模板的问题在于,对于任何

template <typename T>
class C { };

任何两对C<TypeA>C<TypeB>都是完全不相关的类——如果TypeATypeB只是在const性上不同,情况也是如此。

因此,您真正想要拥有的东西在技术上是不可能的。我现在不会提出新的解决方法,因为已经,但请尝试进一步查看:如评论中所述,您可能会面临 XY 问题。

问题是:用户会如何处理这样的列表?她/他可能正在迭代它 - 或访问单个元素。那为什么不让你的整个班级看起来像一个列表呢?

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(); }
};

如果您使用向量而不是列表,我仍然会提供一个索引运算符:

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

现在有一个问题至今仍未解决:返回的两个const_iterators有一个不可变的共享指针,但点仍然是可变的!

这有点麻烦 - 你现在需要实现自己的迭代器类:

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

请查看std::iterator的完整示例 - 但请注意,std::iterator已被弃用,因此您需要自己实现类型特征。

如果您内部使用std::vector,要使用的迭代器标记将是std::bidirectional_iterator_tagrandom_access_iterator_tag(contiguous_iterator_tag带有 C++20)。

现在重要的是如何实现所需的两个运算符:

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;
}

其他运算符只是将操作转发到内部迭代器(增量、递减(如果可用)、比较等)。

我并不是说这是圣杯,是你在任何情况下都需要遵循的道路。但这是一个有价值的选择,至少值得考虑......