问题 使用动态多态时如何避免指针?


很长一段时间,我已经感知指针, new 和 delete 在C ++中有点不必要,除非处理长寿命对象,引用是更适合RAII模型的更清晰的替代方案。但是,在C ++中使用动态多态时,我仍然无法确定如何避免指针。

我们有这些关系:

class A
{
public:
    virtual void a() const = 0;
};

class B : public A
{
    virtual void a() const
    {
        std::cout << "B";
    }
};

class C : public A
{
    virtual void a() const
    {
        std::cout << "C";
    }
};

void invoke(const A& obj)
{
    obj.a();
}

int main()
{
    B b;
    invoke(b); // Prints B
}

对象可以传递给 invoke 作为参考,并没有涉及指针(好吧,至少从程序员的角度来看)。但是,上面的例子基本上是静态多态。

如果我想做出类型的话 b 依赖于其他东西,我将不得不使用指针:

int main()
{
    A* a;
    if (something)
        a = new B;
    else
        a = new C;

    invoke(*a); // Prints B
    delete a;
}

对我来说,这看起来很难看。当然,我可以使用智能指针:

int main()
{
    std::unique_ptr<A> a;
    if (something)
        a.reset(new B);
    else
        a.reset(new C);
    invoke(*a); // Prints B
}

但智能指针只是指针的包装器。

我想知道是否有办法避免这种情况并在不使用指针的情况下利用多态类。


11350
2017-08-31 10:15


起源

if(cond) invoke(B(...)) else invoke(C(...)),又称功能风格?或者在带有lambdas的C ++ 11中,如果公共部分很大并且你不想要一个额外的全局函数。 - Xeo
@Xeo它可以在这个特定的例子中工作,但这并不总是适用。 - Tibor
你所谓的静态多态性根本不是静态多态。 invoke 并不准确地知道其论证的类型。只有你 认为 它是静态多态,因为您可以看到代码和传递的类型! - Hbcdev
@Tibor:这在你需要有条件地存储一个或另一个对象的情况下不起作用,这是真的。在这些情况下,您不会使用指针,因为它们对动态多态性至关重要。 - Xeo
@Hbcdev:我认为OP意味着排除动态绑定可以被元编程取代的所有情况,因为所有基于运行时类型的决策都可以在编译时知道。第二个对象的实际类型取决于运行时信息,您放弃了一些优化选项。因此,如果 something 是一个 constexpr,你 可以 以某种方式称之为“静态多态”。 - bitmask


答案:


你无法避免指出​​这一点。如果你不喜欢它们,C ++不会成为你的语言,因为如果你想做任何多态的事情,你将不得不使用指针来克服最琐碎的用法。在堆上构造对象,即使用 new 是你如何避免堆栈构造对象的作用域生命周期,如果你想在条件分支内部做东西然后将它们分配给父作用域中的变量,你必须这样做 - 如果你不需要这样做你也不要实际上需要多态,因为你的类型在编译时都是可以确定的。没有办法解决这个问题。

当然,使用智能指针,它们确实可以帮助避免指针生命周期的问题,但是无论你最终使用什么样的酷抽象,都会在某处出现指针。


6
2017-08-31 10:38



我越是想到它,它就越有意义。如果不需要指针,就必须对多态类型进行其他严格约束以避免切片,正如BЈовић所指出的那样。 - Tibor


给出你的最后一个例子,你可以避免使用指针(虽然我没有看到智能指针的大问题)

struct AHolder {
  std::shared_ptr<A> a;
  operator A const&() const {
    return *a;
  }
  AHolder(bool something)
  : a(something?std::make_shared<B>():std::make_shared<C>()) {
  }
};

这允许您使用持有者 - 如果它是实际对象:

int main() {
  AHolder a(true); // or whatever argument you like
  invoke(a);
}

4
2017-08-31 10:27



当然,这会奏效。然而,我的问题是更多关于避免使用 new 和 * 操作员。 - Tibor
@Tibor:嗯,好的 * 类型限定符以及 * 运营商是 隐 在这个工具中(可以很容易地制作成通用模板),就像智能​​指针隐藏他们对原始指针的使用一样。 - bitmask
真的很好的例子! - besworland


我不知道为什么你要不惜一切代价避免任何形式的指针。使用起来绝对没问题 std::unique_ptr / std::shared_ptr (当有需要的时候)。 智能指针不是指针周围的“包裹”。它们允许您在不同的语义之间进行选择,并具有不同的用途。


1
2017-08-31 10:25





您可能正在考虑的用例是某种用例 ,你可以很好地使用智能指针,没有 new 在所有:

std::unique_ptr<Message> parse_message(char const * buf, std::size_t len)
{
    if (buf[0] == 'A') { return make_unique<RequestMsg>(buf + 1, len - 1); }

    if (buf[0] == 'R') { return make_unique<AnswerMsg>(buf + 1, len - 1); }

    return nullptr;
}

用法:

auto msgptr = parse_msg(buf, len);

(你需要有一个定义 make_unique 在某个地方,遗憾的是,目前的标准库遗漏了它,但最终会被修改。)


1
2017-08-31 10:35



的定义 make_unique 包含一个电话 new 虽然。从根本上说,你无法摆脱解决这个问题的指针,尽管你可以做出各种漂亮而有用的抽象。 - Matthew Walton
+1让我意识到我忘记了 make_ 例程(并使用脏ol' new)。 - bitmask
@MatthewWalton:是的,当然。并且标准库中的每个动态容器都会调用 ::new 左右中心。但这里的重点是建立一个将军 new - 和C ++的无指针成语!并牢记包装函数在异常安全方面的差异。 - Kerrek SB


我想知道是否有办法避免这种情况并在不使用指针的情况下利用多态类。

不,这就是c ++首先设计的方式。为避免切片,您需要使用指针或引用。


1
2017-08-31 10:35





如果您需要动态创建对象并使它们远离函数范围,则无法实现。你可以做到最好 - 在工厂和智能指针中隐藏对象创建。

您也可以在功能范围内考虑静态对象,但这不是一个好的解决方案 - 您可以通过这种方式创建有限数量的对象:

template <class T, int Instance> T& create()
{
   static T instance;
   return instance;
}

MyClass& obj1 = create<MyClass, 1>();
MyClass& obj2 = create<MyClass, 2>(); 
MyClass& obj3 = create<MyClass, 3>(); 
// etc

该解决方案具有一些限制,很少可以使用。请注意,对象已创建 静态 在第一次函数调用时,将一直持续到程序结束。


0
2017-08-31 10:38



单身人士通常很难看,在这种情况下,特别危险。这实际上是amost-singleton对象的生成器。 - bitmask
@bitmask当然,那是因为我写这个是可能的,但不是很好的解决方案。请注意,实际上你 能够 创造了很多相同类型的对象,所以它不是关于单身人士...... - Rost
和 那这就是我没有投票的原因。那个同意我之前评论的人也没有。 - bitmask