You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

请教Sean Parent:inheritance hierarchy中polymorphic types的可变对象为何是极端例外

Understanding Sean Parent’s Advice on Immutable Polymorphic Types

Great question—let’s unpack Sean Parent’s key insight here, because it cuts to the heart of writing maintainable, safe polymorphic code (especially in C++, where his work on runtime polymorphism and value semantics is widely respected).

First, let’s restate his quote in plain terms:

"for polymorphic types in an inheritance hierarchy, having mutable object is the extreme exception"

What this means is: When you’re working with a class hierarchy designed for polymorphism (where you use base class pointers/references to interact with subclass instances), you should almost always make those objects immutable. Mutable state should be a rare, intentional choice, not the default.


The Two Core Reasons Sean Cites

Let’s break down the two key rationales behind this advice, in practical terms:

1. Mutable polymorphic objects invite unintended side effects and break encapsulation

Polymorphic objects are typically accessed through base class interfaces. If that interface includes methods to modify state, the caller has no way of knowing exactly how the subclass will handle that modification. This leads to unpredictable behavior—for example, a setSize() method might trigger expensive re-computations in one subclass, or invalidate cached data in another, without the caller being aware.

Worse, if multiple parts of your code hold references to the same polymorphic object, a modification in one place can silently break other parts that depend on the object’s previous state. This makes debugging a nightmare and violates the principle of least surprise.

2. Immutable polymorphic objects play nicely with value semantics

Sean Parent is a huge advocate of value semantics (objects that behave like integers—copyable, moveable, and self-contained) because they simplify code and avoid common pitfalls like dangling references or resource leaks.

Mutable polymorphic objects are hard to fit into value semantics: copying them requires deep copies (which are expensive or error-prone), and sharing references risks accidental modification. Immutable objects, on the other hand, can be safely shared (since their state never changes) or copied without worry—you don’t have to worry about synchronizing state across copies, because there’s nothing to synchronize.


Why Can’t Subclasses Expose State-Modifying Functions?

It’s not that subclasses can’t modify their own state—it’s that you shouldn’t expose those modifications through the polymorphic base class interface. Here’s why:

If you add a mutable method like setPosition() to your base Shape class, every subclass must implement it, even if modifying position doesn’t make sense for that subclass (e.g., a FixedShape that shouldn’t move). This forces subclasses to either implement a no-op (which is confusing) or throw an exception (which is error-prone).

Instead, if you need to modify state, use a pattern that preserves immutability: return a new instance of the object with the updated state, rather than modifying the existing one. This way, the polymorphic interface only exposes read-only operations, and subclasses can handle state changes internally by creating new copies.

Example: Immutable Polymorphism Done Right

Bad approach (mutable polymorphic interface):

class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
    virtual void setPosition(int x, int y) = 0; // ❌ Forces mutability on all subclasses
};

class Circle : public Shape {
    int x_, y_;
    int radius_;
public:
    void draw() const override { /* ... */ }
    void setPosition(int x, int y) override { x_ = x; y_ = y; }
};

Good approach (immutable with state-updating factory methods):

#include <memory>

class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
    // Return a new Shape instance with updated position
    virtual std::unique_ptr<Shape> withPosition(int x, int y) const = 0;
    // Read-only accessors for state
    virtual int x() const = 0;
    virtual int y() const = 0;
};

class Circle : public Shape {
    int x_, y_;
    int radius_;
public:
    Circle(int x, int y, int r) : x_(x), y_(y), radius_(r) {}
    
    void draw() const override { /* ... */ }
    
    std::unique_ptr<Shape> withPosition(int x, int y) const override {
        // Return a new Circle with updated position, leaving the original unchanged
        return std::make_unique<Circle>(x, y, radius_);
    }
    
    int x() const override { return x_; }
    int y() const override { return y_; }
};

In this pattern, the original Circle instance stays immutable, but you can create a new instance with the desired state changes. This keeps the polymorphic interface clean, avoids side effects, and aligns with Sean’s advice.


Final Takeaway

Sean’s advice boils down to this: Polymorphism is about behavior, not state changes. By making polymorphic objects immutable by default, you eliminate a host of bugs related to side effects, concurrency, and broken encapsulation. When you do need to modify state, prefer creating new instances over mutating existing ones—it’s safer, more predictable, and plays better with modern C++ practices.

内容的提问来源于stack exchange,提问作者CD86

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:16:54