使用std::unique_ptr / std::shared_ptr实现正确的组合

3

目前我在思考如何正确地使用std :: unique_ptr 作为成员变量,关于const的正确性。

以下示例允许更改由my_foo拥有的内容,尽管它是const:

#include <iostream>
#include <memory>

struct foo {
    foo() : value_ptr_(std::make_unique<int>(3)) {}
    void increment() const {
        ++(*value_ptr_);
    }
    int get_value() const {
        return *value_ptr_;
    }
    std::unique_ptr<int> value_ptr_;
};

int main() {
    const foo my_foo;
    std::cout << my_foo.get_value() << std::endl;
    my_foo.increment(); // But my_foo is const!
    std::cout << my_foo.get_value() << std::endl;
}

std::make_unique<const T>替换std::make_unique<T>似乎是一个不错的解决方案。然而,这会禁止更改my_foo的内容,即使它是非常量:
#include <iostream>
#include <memory>

struct foo {
    foo() : value_ptr_(std::make_unique<int>(3)) {}
    void increment() {
        ++(*value_ptr_);
    }
    int get_value() const {
        return *value_ptr_;
    }
    std::unique_ptr<const int> value_ptr_;
};

int main() {
    foo my_foo;
    std::cout << my_foo.get_value() << std::endl;
    my_foo.increment(); // compiler error
    std::cout << my_foo.get_value() << std::endl;
}

在这个简单的例子中,拥有一个指向int的指针显然没有太多意义,但在实际代码中,unique_ptr可能会持有指向多态对象的基类指针,即我们无法通过简单存储值来存储的对象。

那么如何更好地处理这种情况呢?


1
我不确定为什么您允许在“const foo”对象上调用“increment”?根据名称,在这种情况下不应允许调用它。 - UnholySheep
我想你可以像使用原始指针一样去处理它,使用某种包装类。 - juanchopanza
3
似乎std::experimental::propagate_const就是为此而设计的。但我不具备足够的知识来编写答案。 - user2486888
@UnholySheep 对我来说问题在于,我被允许在第一次写入“increment”时使用“const”说明符。 - Tobias Hermann
@NickyC,是的,看起来std::experimental::propagate_const正是我要找的。不过似乎我得等一段时间才能使用它。;-) - Tobias Hermann
@TobiasHermann 这很容易自己实现。 - Richard Hodges
2个回答

3

您可以继承 std::unique_ptr 并覆盖仅 3 个方法(对于 unique_ptr<T[]> 来说是 4 个),提供 const/non-const 重载:

template <typename T>
struct propagating_unique_ptr : std::unique_ptr<T> {
    using unique_ptr<T>::unique_ptr;
    using unique_ptr<T>::operator =;

    const T *get() const noexcept {
        return unique_ptr<T>::get();
    }
    T *get() noexcept {
        return unique_ptr<T>::get();
    }

    const T &operator *() const noexcept {
        return unique_ptr<T>::operator *();
    }
    T &operator *() noexcept {
        return unique_ptr<T>::operator *();
    }

    const T *operator -> () const noexcept {
        return unique_ptr<T>::get();
    }
    T *operator -> () noexcept {
        return unique_ptr<T>::get();
    }
}; 

非常感谢。您的解决方案非常好,我喜欢它,因为模式被抽象化了,实际类 foo 不必修改太多。 :) 测试1 测试2 测试3 - Tobias Hermann

1
我通过提供一个内部协议来提供访问正确构建的底层实现引用的方式来实现这一点。
类似于这样:
struct foo {
    // standard (in your codebase) protocol to express the impl base class
    using underlying_impl = int;

    // standard protocol to express ownership semantics
    using implementation_handle = std::unique_ptr<underlying_impl>;

    // construction via private 'construct' protocol    
    foo() : value_ptr_(construct(3)) {}

    // all internal access to the implementation via a the protocol
    // of get_impl()
    auto operator++() -> foo&
    {
        // not-const - compiles fine
        ++get_impl();
        return *this;
    }

    void increment() const {
// now won't compile - get_impl() propagates const correctly
//        ++get_impl();
    }

private:

    static auto construct(int val) -> implementation_handle
    {
        return std::make_unique<underlying_impl>(val);
    }

    // two versions of get_impl() - const and mutable
    auto get_impl() const -> underlying_impl const&
    {
        return *value_ptr_;
    }

    auto get_impl() -> underlying_impl&
    {
        return *value_ptr_;
    }

    // actual storage of the implementation handle
    implementation_handle value_ptr_;
};

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