在抽象接口中不转移所有权的指针向量的常量正确访问器
Const-correct accessor to vector of pointers without transfer of ownership in abstract interface
我正在从头开始设计一个库,希望 public API 尽可能 好 。我希望编译器因滥用而对我大喊大叫。因此,我给自己规定了以下规则:
整个库的真正(即深入和完整)const 正确性
声明所有事物(局部变量、成员变量、成员函数),预计不会改变const
。该常量性应传播到所有嵌套成员和类型。
明确和表达所有权
根据 C++ 核心指南,我将其定义为(iff 在 的数学意义上当且仅当 ):
- 函数参数是
unique_ptr<T>
或 T&&
iff 函数正在使用它(即取得所有权)
- 函数参数是
shared_ptr<const T>
或T const&
iff函数只读取它
- 函数参数是
shared_ptr<T>
或 T&
iff 函数正在修改它而不取得所有权
- return 值为
unique_ptr<T>
或 T
iff 函数将所有权转移给调用者
- return 值是
shared_ptr<const T>
或 T const&
iff 调用者应该只读取它(尽管调用者可以构造它的副本- 鉴于 T
是可复制的)
- 任何函数都不应该 return
shared_ptr<T>
、T&
或 T*
(因为它会导致无法控制的副作用,我试图通过设计避免)
隐藏实现细节
现在我将使用工厂的抽象接口 return 将实现作为 unique_ptr<Interface>
。不过,我愿意接受可以解决下面描述的问题的替代模式。
我不关心虚拟 table 查找,并希望通过各种方式避免动态转换(我认为它们是一种代码味道)。
现在,给定两个 类 A
和 B
,其中 B
拥有可变数量的 A
。我们还有 B
-实现 BImpl
(A
的实现在这里可能没有用):
class A
{};
class B {
public:
virtual ~B() = default;
virtual void addA(std::unique_ptr<A> aObj) = 0;
virtual ??? aObjs() const = 0;
};
class BImpl : public B {
public:
virtual ~BImpl() = default;
void addA(std::unique_ptr<A> aObj) override;
??? aObjs() const override;
private:
std::vector<unique_ptr<A>> aObjs_;
};
我坚持使用 B
s getter 的 return 值到 A
s 的向量:aObjs()
.
它应该提供 A
s 的列表作为只读值而不转移所有权(规则 2.5。上面的 const 正确性)并且仍然为调用者提供对所有 A
s 的轻松访问,例如用于基于范围的 for
或标准算法,例如 std::find
.
我为那些 ???
提出了以下选项:
std::vector<std::shared_ptr<const A>> const&
每次调用 aObjs()
时我都必须构建一个新向量(我可以将它缓存在 BImpl
中)。这感觉不仅效率低下而且不必要地复杂,而且似乎也不是最理想的。
用一对函数(aObjsBegin()
和aObjsEnd()
)替换aObjs()
转发BImpl::aObjs_
.
的常量迭代器
等等。我需要将 unique_ptr<A>::const_iterator
设为 unique_ptr<const A>::const_iterator
以获得我心爱的 const 正确性。再次令人讨厌的演员表或中间对象。并且用户无法在基于范围的 for
.
中轻松使用它
我缺少什么明显的解决方案?
编辑:
B
应该总是能够修改它持有的 A
s,因此将 aObjs_
声明为 vector<std::unique_ptr<const A>>
不是一个选项。
让 B
坚持迭代器概念来迭代 A
s,这两种选择都不是一个选项,因为 B
将包含 [=59] 的列表=]s 和特定的 D
(或 none)。
您可以 return 向量的包装器,而不是直接尝试 return 向量,它允许您仅使用 const 指针访问内容。这听起来可能很复杂,但事实并非如此。只需制作一个薄包装器并添加一个 begin()
和 end()
函数以允许迭代:
struct BImpl : B {
virtual ~BImpl() = default;
void addA(std::unique_ptr<A> aObj) override;
ConstPtrVector<A> aObjs() const override {
return aObjs_;
}
private:
std::vector<unique_ptr<A>> aObjs_;
};
ConstPtrVector
将如下所示:
template<typename T>
ConstPtrVector {
ConstPtrVector(const std::vector<T>& vec_) : vec{vec_} {}
MyConstIterator<T> begin() const {
return vec.begin();
}
MyConstIterator<T> end() const {
return vec.end();
}
private:
const std::vector<T>& vec;
};
并且您可以以将 return 指针作为 const:
的方式实现 MyConstIterator
template<typename T>
struct MyConstIterator {
MyConstIterator(std::vector<unique_ptr<T>>::const_iterator it_) : it{std::move(it_)} {}
bool operator==(const MyConstIterator& other) const {
return other.it == it;
}
bool operator!=(const MyConstIterator& other) const {
return other.it != it;
}
const T* operator*() const {
return it->get();
}
const T* operator->() const {
return it->get();
}
MyConstIterator& operator++() {
++it;
return *this;
}
MyConstIterator& operator--() {
--it;
return *this;
}
private:
std::vector<unique_ptr<T>>::const_iterator it;
};
当然,您可以通过实现类似接口的向量来概括此迭代器和包装器。
然后,要使用它,您可以使用基于范围的 for 循环或基于经典迭代器的循环。
顺便说一句:不拥有原始指针并没有错。只要他们仍然不拥有。如果你想避免由于原始指针导致的错误,请查看 observer_ptr<T>
,它可能会有用。
有了range-v3,你可以做到
template <typename T>
using const_view_t = decltype(std::declval<const std::vector<std::unique_ptr<T>>&>()
| ranges::view::transform(&std::unique_ptr<T>::get)
| ranges::view::indirect);
class B
{
public:
virtual ~B() = default;
virtual void addA(std::unique_ptr<A> a) = 0;
virtual const_view_t<A> getAs() const = 0;
};
class D : public B
{
public:
void addA(std::unique_ptr<A> a) override { v.emplace_back(std::move(a)); }
const_view_t<A> getAs() const override {
return v | ranges::view::transform(&std::unique_ptr<A>::get)
| ranges::view::indirect;
}
private:
std::vector<std::unique_ptr<A>> v;
};
然后
for (const A& a : d.getAs()) {
std::cout << a.n << std::endl;
}
template<class It>
struct range_view_t {
It b{};
It e{};
range_view_t(It s, It f):b(std::move(s)), e(std::move(f)) {}
range_view_t()=default;
range_view_t(range_view_t&&)=default;
range_view_t(range_view_t const&)=default;
range_view_t& operator=(range_view_t&&)=default;
range_view_t& operator=(range_view_t const&)=default;
It begin() const { return b; }
It end() const { return e; }
};
这里我们从一系列迭代器开始。
我们可以通过 range_view_t remove_front(std::size_t n = 1)const
、bool empty() const
、front()
等使它更丰富
我们可以使用通常的技术对其进行扩充,如果 It
具有 random_access_iterator_tag
类别,则有条件地添加 operator[]
和 size
,并静默添加 remove_front
绑定 n
.
然后更进一步,我们写array_view_t
:
template<class T>
struct array_view_t:range_view<T*> {
using range_view<T*>::range_view;
array_view_t()=default; // etc
array_view_t( T* start, std::size_t length ):array_view_t(start, start+length) {}
template<class C,
std::enable_if_t
std::is_same< std::remove_pointer_t<data_type<C>>, T>{}
|| std::is_same< const std::remove_pointer_t<data_type<C>>, T>{},
, int
> =0
>
array_view_t( C& c ):array_view_t(c.data(), c.size()) {}
template<std::size_t N>
array_view_t( T(&arr)[N] ):array_view_t( arr, N ) {}
};
抽象查看连续容器的内容。
现在你的 BImpl
return 是 array_view_t< const std::unique_ptr<A> >
。
这个抽象级别基本上是免费的。
如果这还不够,您键入 T
的擦除随机访问迭代,然后 return a range_view_t< any_random_access_iterator<T> >
,在这种情况下 T
是 const std::unique_ptr<A>
.
我们也可以删除所有权语义,并在放入范围适配器后成为 range_view_t< any_random_access_iterator<A*> >
。
这种级别的类型擦除不是免费的。
对于彻底的疯狂,您可以停止使用智能指针或接口。
使用类型擦除描述您的接口。传出任何用类型擦除包裹的。几乎所有东西都使用值语义。如果您消费一个副本,按值取用,然后移出该值。避免对对象的持久引用。短期引用是引用,如果它们是可选的,则为指针。这些没有存储。
当您无力使用值时,使用名称而不是地址,并使用某处的注册表来获取项目。
我正在从头开始设计一个库,希望 public API 尽可能 好 。我希望编译器因滥用而对我大喊大叫。因此,我给自己规定了以下规则:
整个库的真正(即深入和完整)const 正确性
声明所有事物(局部变量、成员变量、成员函数),预计不会改变
const
。该常量性应传播到所有嵌套成员和类型。明确和表达所有权
根据 C++ 核心指南,我将其定义为(iff 在 的数学意义上当且仅当 ):
- 函数参数是
unique_ptr<T>
或T&&
iff 函数正在使用它(即取得所有权) - 函数参数是
shared_ptr<const T>
或T const&
iff函数只读取它 - 函数参数是
shared_ptr<T>
或T&
iff 函数正在修改它而不取得所有权 - return 值为
unique_ptr<T>
或T
iff 函数将所有权转移给调用者 - return 值是
shared_ptr<const T>
或T const&
iff 调用者应该只读取它(尽管调用者可以构造它的副本- 鉴于T
是可复制的) - 任何函数都不应该 return
shared_ptr<T>
、T&
或T*
(因为它会导致无法控制的副作用,我试图通过设计避免)
- 函数参数是
隐藏实现细节
现在我将使用工厂的抽象接口 return 将实现作为
unique_ptr<Interface>
。不过,我愿意接受可以解决下面描述的问题的替代模式。
我不关心虚拟 table 查找,并希望通过各种方式避免动态转换(我认为它们是一种代码味道)。
现在,给定两个 类 A
和 B
,其中 B
拥有可变数量的 A
。我们还有 B
-实现 BImpl
(A
的实现在这里可能没有用):
class A
{};
class B {
public:
virtual ~B() = default;
virtual void addA(std::unique_ptr<A> aObj) = 0;
virtual ??? aObjs() const = 0;
};
class BImpl : public B {
public:
virtual ~BImpl() = default;
void addA(std::unique_ptr<A> aObj) override;
??? aObjs() const override;
private:
std::vector<unique_ptr<A>> aObjs_;
};
我坚持使用 B
s getter 的 return 值到 A
s 的向量:aObjs()
.
它应该提供 A
s 的列表作为只读值而不转移所有权(规则 2.5。上面的 const 正确性)并且仍然为调用者提供对所有 A
s 的轻松访问,例如用于基于范围的 for
或标准算法,例如 std::find
.
我为那些 ???
提出了以下选项:
std::vector<std::shared_ptr<const A>> const&
每次调用
aObjs()
时我都必须构建一个新向量(我可以将它缓存在BImpl
中)。这感觉不仅效率低下而且不必要地复杂,而且似乎也不是最理想的。用一对函数(
的常量迭代器aObjsBegin()
和aObjsEnd()
)替换aObjs()
转发BImpl::aObjs_
.等等。我需要将
unique_ptr<A>::const_iterator
设为unique_ptr<const A>::const_iterator
以获得我心爱的 const 正确性。再次令人讨厌的演员表或中间对象。并且用户无法在基于范围的for
. 中轻松使用它
我缺少什么明显的解决方案?
编辑:
B
应该总是能够修改它持有的A
s,因此将aObjs_
声明为vector<std::unique_ptr<const A>>
不是一个选项。让
B
坚持迭代器概念来迭代A
s,这两种选择都不是一个选项,因为B
将包含 [=59] 的列表=]s 和特定的D
(或 none)。
您可以 return 向量的包装器,而不是直接尝试 return 向量,它允许您仅使用 const 指针访问内容。这听起来可能很复杂,但事实并非如此。只需制作一个薄包装器并添加一个 begin()
和 end()
函数以允许迭代:
struct BImpl : B {
virtual ~BImpl() = default;
void addA(std::unique_ptr<A> aObj) override;
ConstPtrVector<A> aObjs() const override {
return aObjs_;
}
private:
std::vector<unique_ptr<A>> aObjs_;
};
ConstPtrVector
将如下所示:
template<typename T>
ConstPtrVector {
ConstPtrVector(const std::vector<T>& vec_) : vec{vec_} {}
MyConstIterator<T> begin() const {
return vec.begin();
}
MyConstIterator<T> end() const {
return vec.end();
}
private:
const std::vector<T>& vec;
};
并且您可以以将 return 指针作为 const:
的方式实现MyConstIterator
template<typename T>
struct MyConstIterator {
MyConstIterator(std::vector<unique_ptr<T>>::const_iterator it_) : it{std::move(it_)} {}
bool operator==(const MyConstIterator& other) const {
return other.it == it;
}
bool operator!=(const MyConstIterator& other) const {
return other.it != it;
}
const T* operator*() const {
return it->get();
}
const T* operator->() const {
return it->get();
}
MyConstIterator& operator++() {
++it;
return *this;
}
MyConstIterator& operator--() {
--it;
return *this;
}
private:
std::vector<unique_ptr<T>>::const_iterator it;
};
当然,您可以通过实现类似接口的向量来概括此迭代器和包装器。
然后,要使用它,您可以使用基于范围的 for 循环或基于经典迭代器的循环。
顺便说一句:不拥有原始指针并没有错。只要他们仍然不拥有。如果你想避免由于原始指针导致的错误,请查看 observer_ptr<T>
,它可能会有用。
有了range-v3,你可以做到
template <typename T>
using const_view_t = decltype(std::declval<const std::vector<std::unique_ptr<T>>&>()
| ranges::view::transform(&std::unique_ptr<T>::get)
| ranges::view::indirect);
class B
{
public:
virtual ~B() = default;
virtual void addA(std::unique_ptr<A> a) = 0;
virtual const_view_t<A> getAs() const = 0;
};
class D : public B
{
public:
void addA(std::unique_ptr<A> a) override { v.emplace_back(std::move(a)); }
const_view_t<A> getAs() const override {
return v | ranges::view::transform(&std::unique_ptr<A>::get)
| ranges::view::indirect;
}
private:
std::vector<std::unique_ptr<A>> v;
};
然后
for (const A& a : d.getAs()) {
std::cout << a.n << std::endl;
}
template<class It>
struct range_view_t {
It b{};
It e{};
range_view_t(It s, It f):b(std::move(s)), e(std::move(f)) {}
range_view_t()=default;
range_view_t(range_view_t&&)=default;
range_view_t(range_view_t const&)=default;
range_view_t& operator=(range_view_t&&)=default;
range_view_t& operator=(range_view_t const&)=default;
It begin() const { return b; }
It end() const { return e; }
};
这里我们从一系列迭代器开始。
我们可以通过 range_view_t remove_front(std::size_t n = 1)const
、bool empty() const
、front()
等使它更丰富
我们可以使用通常的技术对其进行扩充,如果 It
具有 random_access_iterator_tag
类别,则有条件地添加 operator[]
和 size
,并静默添加 remove_front
绑定 n
.
然后更进一步,我们写array_view_t
:
template<class T>
struct array_view_t:range_view<T*> {
using range_view<T*>::range_view;
array_view_t()=default; // etc
array_view_t( T* start, std::size_t length ):array_view_t(start, start+length) {}
template<class C,
std::enable_if_t
std::is_same< std::remove_pointer_t<data_type<C>>, T>{}
|| std::is_same< const std::remove_pointer_t<data_type<C>>, T>{},
, int
> =0
>
array_view_t( C& c ):array_view_t(c.data(), c.size()) {}
template<std::size_t N>
array_view_t( T(&arr)[N] ):array_view_t( arr, N ) {}
};
抽象查看连续容器的内容。
现在你的 BImpl
return 是 array_view_t< const std::unique_ptr<A> >
。
这个抽象级别基本上是免费的。
如果这还不够,您键入 T
的擦除随机访问迭代,然后 return a range_view_t< any_random_access_iterator<T> >
,在这种情况下 T
是 const std::unique_ptr<A>
.
我们也可以删除所有权语义,并在放入范围适配器后成为 range_view_t< any_random_access_iterator<A*> >
。
这种级别的类型擦除不是免费的。
对于彻底的疯狂,您可以停止使用智能指针或接口。
使用类型擦除描述您的接口。传出任何用类型擦除包裹的。几乎所有东西都使用值语义。如果您消费一个副本,按值取用,然后移出该值。避免对对象的持久引用。短期引用是引用,如果它们是可选的,则为指针。这些没有存储。
当您无力使用值时,使用名称而不是地址,并使用某处的注册表来获取项目。