shared_ptr<T> 转换为 shared_ptr<T const>,以及 vector<T> 转换为 vector<T const>。

11

我正在尝试为我的软件定义一个良好的设计,这意味着需要小心处理一些变量的读写访问。这里我简化了程序以便讨论。希望这对其他人也有所帮助。 :-)

假设我们有一个如下的类X:

class X {
    int x;
public:
    X(int y) : x(y) { }
    void print() const { std::cout << "X::" << x << std::endl; }
    void foo() { ++x; }
};

假设今后这个类将被子类 X1,X2 等继承,并重新实现 print() 和 foo() 方法。(我省略了 virtual 关键字只是为了方便,因为这不是我遇到的实际问题。)

由于我们将使用多态性,让我们使用 (智能) 指针并定义一个简单工厂:

using XPtr = std::shared_ptr<X>;
using ConstXPtr = std::shared_ptr<X const>;

XPtr createX(int x) { return std::make_shared<X>(x); }

目前为止,一切都很好:我可以定义goo(p),它可以读写p,并且还可以定义hoo(p),它只能读取p

void goo(XPtr p) {
    p->print();
    p->foo();
    p->print();
}

void hoo(ConstXPtr p) {
    p->print();
//    p->foo(); // ERROR :-)
}

而调用站点看起来像这样:

    XPtr p = createX(42);

    goo(p);
    hoo(p);

指向 X 的 shared pointer(XPtr)会自动转换为其 const 版本(ConstXPtr)。太好了,这正是我想要的!

现在遇到麻烦了:我需要一个 X 的异构集合。我选择使用 std::vector<XPtr>。(也可以是 list,为什么不呢。)

我考虑的设计如下。我有两个版本的容器:一个可读写访问其元素,一个只能以只读方式访问其元素。

using XsPtr = std::vector<XPtr>;
using ConstXsPtr = std::vector<ConstXPtr>;

我有一个处理这些数据的类:

class E {
    XsPtr xs;
public:
    E() {
        for (auto i : { 2, 3, 5, 7, 11, 13 }) {
            xs.emplace_back(createX(std::move(i)));
        }
    }

    void loo() {
        std::cout << "\n\nloo()" << std::endl;
        ioo(toConst(xs));

        joo(xs);

        ioo(toConst(xs));
    }

    void moo() const {
        std::cout << "\n\nmoo()" << std::endl;
        ioo(toConst(xs));

        joo(xs); // Should not be allowed

        ioo(toConst(xs));
    }
};

ioo()joo() 函数如下:

void ioo(ConstXsPtr xs) {
    for (auto p : xs) {
        p->print();
//        p->foo(); // ERROR :-)
    }
}

void joo(XsPtr xs) {
    for (auto p: xs) {
        p->foo();
    }
}

正如您所看到的,在 E::loo()E::moo() 中,我需要使用 toConst() 进行一些转换:

ConstXsPtr toConst(XsPtr xs) {
    ConstXsPtr cxs(xs.size());
    std::copy(std::begin(xs), std::end(xs), std::begin(cxs));
    return cxs;
}

但这意味着一遍又一遍的复制... :-/

还有,对于 moo() 这个被声明为 const 的函数,我却可以调用 joo() 来修改 xs 的数据。这不是我想要的。我希望在这里能够出现编译错误。

完整代码可在 ideone.com 查看。

问题是:是否可能在不将向量复制到其 const 版本的情况下完成相同的操作?或者更一般地说,是否有一种既高效又易于理解的技术/模式?

谢谢。 :-)


使用boost::adaptors::transformed和合适的函数对象获取一个带有const视图,以转换您的共享指针。 - Xeo
@Xeo:我快速查看了boost::adaptors::transformed,但似乎在某个时候我必须复制一些东西,有点像上面的代码但语法不同,对吗?如果不是这样,您能否在下面给出一个示例呢? :-) - Hiura
只是提醒一下,你的 std::move(i) 不会移动任何东西。move 并不会移动,它只是一个转换。也许这只是因为你复制了实际移动代码的缘故 :) - typ1232
4个回答

6
我认为通常的答案是,对于一个类模板 X<T>,可以专门处理任何 X<const T>,因此编译器不允许简单地假定它可以将指向 X<T> 的指针或引用转换为 X<const T>,并且没有一般的方法来表示这两个实际上是可转换的。但是我想到:等等,有一种方法可以说 X<T>X<const T>。"IS A" 通过继承表达。
虽然这对于 std::shared_ptr 或标准容器可能没有帮助,但这是您在实现自己的类时可能要使用的技术。实际上,我想知道是否可以/应该改进 std::shared_ptr 和容器以支持此功能。有人能看出任何问题吗?
我考虑的技术将像这样工作:
template< typename T > struct my_ptr : my_ptr< const T >
{
    using my_ptr< const T >::my_ptr;
    T& operator*() const { return *this->p_; }
};

template< typename T > struct my_ptr< const T >
{
protected:
    T* p_;

public:
    explicit my_ptr( T* p )
      : p_(p)
    {
    }

    // just to test nothing is copied
    my_ptr( const my_ptr& p ) = delete;

    ~my_ptr()
    {
        delete p_;
    }

    const T& operator*() const { return *p_; }
};

Live example


你尝试过编译它吗? - user2026095
1
std::shared_ptr<T>可以通过template< class Y > shared_ptr( const shared_ptr<Y>& r );转换为其const版本。这就是我在问题的第一部分中使用goo()hoo()时发生的情况。所以我想我不必重新创造共享指针类,对吧? - Hiura
另外,我是否正确理解了您的答案:我应该以类似于 m_ptr 的方式拥有一个自定义容器类? - Hiura
1
@Hiura 现有的 shared_ptr<T> 转换shared_ptr<const T>,这意味着会创建一个新的 std::shared_ptr<const T> 实例,这会在运行时产生影响。实际生成了代码,包括引用计数。如果您的代码不太关注性能,可以使用它。我展示的技术并不是直接解决您的问题的方法,因为这意味着需要修复标准和标准库的实现。如果被使用,转换的性能影响将消失。 - Daniel Frey
1
当你添加.reset(T*)方法时,基本问题就暴露出来了:T const版本想要允许.reset(T const*),但无法安全地执行此操作。你需要将指针值和内容的读取和写入分别拆分为不同的类,总共需要2x2=4个类(也许3个,因为你可以使用足够的const_cast合并其中的2个)。 - Yakk - Adam Nevraumont
显示剩余2条评论

1

Hiura,

我试着从代码库中编译你的代码,但是g++4.8返回了一些错误。需要更改main.cpp的第97行和其余调用view::create()的代码,将lambda函数作为第二个参数传递。

+添加+
auto f_lambda([](view::ConstRef_t<view::ElementType_t<Element>> const& e) { return ((e.getX() % 2) == 0); });

std::function<bool(view::ConstRef_t<view::ElementType_t<Element>>)> f(std::cref(f_lambda));

+mod+

printDocument(view::create(xs, f));

同时,View.hpp:185需要添加一个额外的运算符,即+add+

bool operator==(IteratorBase const& a, IteratorBase const& b)
{
  return a.self == b.self;
}

BR, Marek Szews


谢谢你的提醒,但是如果你想遵循 Stack Overflow 的精神,你应该在 Github 上开一个 issue 而不是在这里回答。 - Hiura

1
你想实现的方式存在一个根本问题。
一个`std::vector`并不是`std::vector`的限制,智能指针和其`const`版本的向量也是如此。
具体来说,我可以将指向`const int foo = 7;`的指针存储在第一个容器中,但不能在第二个容器中这样做。 `std::vector`既是范围又是容器。这类似于`T**`与`T const **`的问题。
现在,从技术上讲,`std::vector const`是`std::vector`的限制,但不受支持。
解决这个问题的方法是开始使用范围视图:非拥有视图进入其他容器。可以将非拥有的`T const*`迭代器视图插入`std::vector`中,并为您提供所需的接口。

boost::range 可以为您处理样板文件,但编写自己的 contiguous_range_view<T>random_range_view<RandomAccessIterator> 并不难。当您想要自动检测迭代器类别并基于此启用功能时,它变得复杂起来,这就是为什么 boost::range 包含更多代码的原因。


你能详细说明一下那些 boost::range 吗?也许可以通过展示如何编写 ioo 和 joo 来使用它们,以及调用站点的样子是什么?谢谢。 - Hiura
如果我正确理解了boost文档,那么std::vector<int const*>的替代品应该是boost::iterator_range< std::vector<int*>::const_iterator > - Yakk - Adam Nevraumont

0

根据评论和答案,我最终创建了一个容器视图。

基本上,我定义了新的迭代器。我在github上创建了一个项目: mantognini/ContainerView

代码可能还有改进的余地,但主要思想是,在现有容器(例如 std::vector<T>)上有两个模板类 ViewConstView,它们具有 begin()end() 方法,用于迭代底层容器。

通过一点继承 (ViewConstView),可以在需要时将读写转换为只读视图,而无需额外的代码。

由于我不喜欢指针,所以我使用模板特化来隐藏 std::shared_ptr: 对于 std::shared_ptr<T> 的容器视图不需要额外的解引用。(我还没有为原始指针实现它,因为我不使用它们。)

这是我视图运行的基本示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接