C++ 协变 return 类型的缺点是什么?
What are the drawbacks of C++ covariance return types?
我最近不得不处理 C++ covariance return types 例如以下结构 :
struct Base
{
virtual ~Base();
};
struct Derived : public Base {};
struct AbstractFactory
{
virtual Base *create() = 0;
virtual ~AbstractFactory();
};
struct ConcreteFactory : public AbstractFactory
{
virtual Derived *create()
{
return new Derived;
}
};
它允许客户端代码在需要时将 Derived
对象视为 Base
类型或 Derived
类型,尤其是在不使用 dynamic_cast
或static_cast
.
这种方法有什么缺点?这是设计不良的标志吗?
谢谢。
协方差不适用于智能指针,因此协方差违反了:
切勿通过原始指针 (T*) 或引用 (T&) 转移所有权
的 C++ 核心指南。有一些技巧可以限制这个问题,但协变值仍然是一个原始指针。
文档中的示例:
X* compute(args) // don't
{
X* res = new X{};
// ...
return res;
}
这与问题中的代码所做的几乎相同:
virtual Derived *create()
{
return new Derived;
}
不幸的是,以下内容对于 shared_ptr
和 unique_ptr
都是非法的:
struct AbstractFactory
{
virtual std::shared_ptr<Base> create() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
/*
<source>:16:38: error: invalid covariant return type for 'virtual std::shared_ptr<Derived> ConcreteFactory::create()'
*/
virtual std::shared_ptr<Derived> create()
{
return std::make_shared<Derived>();
}
};
编辑
展示了一种模拟语言协变的技术,以及一些额外的代码。它有一些潜在的维护成本,所以在决定走哪条路之前要考虑到这一点。
在 C++ 中实现的协变 return 类型的主要限制是它们仅适用于原始指针和引用。没有真正的理由不尽可能使用它们,但限制意味着我们不能总是在需要它们时使用它们。
很容易克服这个限制,同时提供相同的用户体验,而无需依赖语言功能。方法如下。
让我们使用常见且流行的 non-virtual interface 习语重写我们的 classes。
struct AbstractFactory
{
Base *create() {
return create_impl();
}
private:
virtual Base* create_impl() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
Derived *create() {
return create_impl();
}
private:
Derived *create_impl() override {
return new Derived;
}
};
现在这里发生了一些有趣的事情。 create
不再是虚拟的,因此可以有任何 return 类型。它不受协变 return 类型规则的约束。 create_impl
仍然受到约束,但它是私有的,除了 class 本身没有人调用它,因此我们可以轻松地操纵它并完全消除协方差。
struct ConcreteFactory : public AbstractFactory
{
Derived *create() {
return create_impl();
}
private:
Base *create_impl() override {
return create_impl_derived();
}
virtual Derived *create_impl_derived() {
return new Derived;
}
};
现在 AbstractFactory
和 ConcreteFactory
都具有与以前完全相同的接口,看不到协变 return 类型。这对我们意味着什么?这意味着我们可以自由使用智能指针。
// replace `sptr` with your favourite kind of smart pointer
struct AbstractFactory
{
sptr<Base> create() {
return create_impl();
}
private:
virtual sptr<Base> create_impl() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
sptr<Derived> create() {
return create_impl();
}
private:
sptr<Base> create_impl() override {
return create_impl_derived();
}
virtual sptr<Derived> create_impl_derived() {
return make_smart<Derived>();
}
};
在这里,我们克服了语言限制,并为我们的 classes 提供了等价的协变 return 类型,而不依赖于有限的语言功能。
注意技术倾向。
sptr<Base> create_impl() override {
return create_impl_derived();
}
这里的函数隐式地将 ("upcasts") Derived
指针转换为 Base
指针。如果我们使用语言提供的协变 return 类型,编译器会在需要时自动插入此类向上转换。不幸的是,该语言的智能程度只能用于原始指针。对于其他一切,我们必须自己做。幸运的是,它相对容易,即使有点冗长。
(在这种特殊情况下,它 可以 只接受 return 一个 Base
指针。我不是在讨论这个。我是假设我们绝对需要协变 return 类型。)
我最近不得不处理 C++ covariance return types 例如以下结构 :
struct Base
{
virtual ~Base();
};
struct Derived : public Base {};
struct AbstractFactory
{
virtual Base *create() = 0;
virtual ~AbstractFactory();
};
struct ConcreteFactory : public AbstractFactory
{
virtual Derived *create()
{
return new Derived;
}
};
它允许客户端代码在需要时将 Derived
对象视为 Base
类型或 Derived
类型,尤其是在不使用 dynamic_cast
或static_cast
.
这种方法有什么缺点?这是设计不良的标志吗?
谢谢。
协方差不适用于智能指针,因此协方差违反了:
切勿通过原始指针 (T*) 或引用 (T&) 转移所有权 的 C++ 核心指南。有一些技巧可以限制这个问题,但协变值仍然是一个原始指针。
文档中的示例:
X* compute(args) // don't
{
X* res = new X{};
// ...
return res;
}
这与问题中的代码所做的几乎相同:
virtual Derived *create()
{
return new Derived;
}
不幸的是,以下内容对于 shared_ptr
和 unique_ptr
都是非法的:
struct AbstractFactory
{
virtual std::shared_ptr<Base> create() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
/*
<source>:16:38: error: invalid covariant return type for 'virtual std::shared_ptr<Derived> ConcreteFactory::create()'
*/
virtual std::shared_ptr<Derived> create()
{
return std::make_shared<Derived>();
}
};
编辑
在 C++ 中实现的协变 return 类型的主要限制是它们仅适用于原始指针和引用。没有真正的理由不尽可能使用它们,但限制意味着我们不能总是在需要它们时使用它们。
很容易克服这个限制,同时提供相同的用户体验,而无需依赖语言功能。方法如下。
让我们使用常见且流行的 non-virtual interface 习语重写我们的 classes。
struct AbstractFactory
{
Base *create() {
return create_impl();
}
private:
virtual Base* create_impl() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
Derived *create() {
return create_impl();
}
private:
Derived *create_impl() override {
return new Derived;
}
};
现在这里发生了一些有趣的事情。 create
不再是虚拟的,因此可以有任何 return 类型。它不受协变 return 类型规则的约束。 create_impl
仍然受到约束,但它是私有的,除了 class 本身没有人调用它,因此我们可以轻松地操纵它并完全消除协方差。
struct ConcreteFactory : public AbstractFactory
{
Derived *create() {
return create_impl();
}
private:
Base *create_impl() override {
return create_impl_derived();
}
virtual Derived *create_impl_derived() {
return new Derived;
}
};
现在 AbstractFactory
和 ConcreteFactory
都具有与以前完全相同的接口,看不到协变 return 类型。这对我们意味着什么?这意味着我们可以自由使用智能指针。
// replace `sptr` with your favourite kind of smart pointer
struct AbstractFactory
{
sptr<Base> create() {
return create_impl();
}
private:
virtual sptr<Base> create_impl() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
sptr<Derived> create() {
return create_impl();
}
private:
sptr<Base> create_impl() override {
return create_impl_derived();
}
virtual sptr<Derived> create_impl_derived() {
return make_smart<Derived>();
}
};
在这里,我们克服了语言限制,并为我们的 classes 提供了等价的协变 return 类型,而不依赖于有限的语言功能。
注意技术倾向。
sptr<Base> create_impl() override {
return create_impl_derived();
}
这里的函数隐式地将 ("upcasts") Derived
指针转换为 Base
指针。如果我们使用语言提供的协变 return 类型,编译器会在需要时自动插入此类向上转换。不幸的是,该语言的智能程度只能用于原始指针。对于其他一切,我们必须自己做。幸运的是,它相对容易,即使有点冗长。
(在这种特殊情况下,它 可以 只接受 return 一个 Base
指针。我不是在讨论这个。我是假设我们绝对需要协变 return 类型。)