Không giống như thừa kế được bảo vệ, thừa kế riêng của C ++ được tìm thấy trong quá trình phát triển C ++ chính thống. Tuy nhiên, tôi vẫn chưa tìm thấy một cách sử dụng tốt cho nó.
Khi nào các bạn sử dụng nó?
Không giống như thừa kế được bảo vệ, thừa kế riêng của C ++ được tìm thấy trong quá trình phát triển C ++ chính thống. Tuy nhiên, tôi vẫn chưa tìm thấy một cách sử dụng tốt cho nó.
Khi nào các bạn sử dụng nó?
Câu trả lời:
Lưu ý sau khi chấp nhận câu trả lời: Đây KHÔNG phải là một câu trả lời hoàn chỉnh. Đọc các câu trả lời khác như ở đây (về mặt khái niệm) và ở đây (cả lý thuyết và thực tiễn) nếu bạn quan tâm đến câu hỏi. Đây chỉ là một thủ thuật ưa thích có thể đạt được với thừa kế tư nhân. Trong khi nó là lạ mắt, nó không phải là câu trả lời cho câu hỏi.
Bên cạnh việc sử dụng cơ bản chỉ thừa kế riêng tư được hiển thị trong Câu hỏi thường gặp về C ++ (được liên kết trong các nhận xét khác), bạn có thể sử dụng kết hợp thừa kế riêng và ảo để đóng dấu một lớp (theo thuật ngữ .NET) hoặc để tạo một lớp cuối cùng (theo thuật ngữ Java) . Đây không phải là một cách sử dụng phổ biến, nhưng dù sao tôi cũng thấy nó thú vị:
class ClassSealer {
private:
friend class Sealed;
ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{
// ...
};
class FailsToDerive : public Sealed
{
// Cannot be instantiated
};
Niêm phong có thể được khởi tạo. Nó xuất phát từ ClassSealer và có thể gọi trực tiếp đến nhà xây dựng riêng vì đây là một người bạn.
FailsToDerive sẽ không biên dịch vì nó phải gọi ClassSealer constructor trực tiếp (yêu cầu thừa kế ảo), nhưng nó không thể vì nó là tư nhân trong Sealed lớp và trong trường hợp này FailsToDerive không phải là một người bạn của ClassSealer .
BIÊN TẬP
Nó đã được đề cập trong các ý kiến rằng điều này không thể được thực hiện chung chung tại thời điểm sử dụng CRTP. Tiêu chuẩn C ++ 11 loại bỏ giới hạn đó bằng cách cung cấp một cú pháp khác để kết bạn với các đối số mẫu:
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
Tất nhiên đây là tất cả, vì C ++ 11 cung cấp một final
từ khóa theo ngữ cảnh cho chính xác mục đích này:
class Sealed final // ...
Tôi sử dụng nó mọi lúc. Một vài ví dụ ngoài đỉnh đầu của tôi:
Một ví dụ điển hình là xuất phát riêng từ một container STL:
class MyVector : private vector<int>
{
public:
// Using declarations expose the few functions my clients need
// without a load of forwarding functions.
using vector<int>::push_back;
// etc...
};
push_back
, hãy MyVector
nhận chúng miễn phí.
template<typename... Args> constexpr decltype(auto) f(Args && ... args) noexcept(noexcept(std::declval<Base &>().f(std::forward<Args>(args)...)) and std::is_nothrow_move_constructible<decltype(std::declval<Base &>().f(std::forward<Args>(args)...))>) { return m_base.f(std::forward<Args>(args)...); }
hoặc bạn có thể viết bằng cách sử dụng Base::f;
. Nếu bạn muốn hầu hết các chức năng và tính linh hoạt mà thừa kế riêng tư và một using
tuyên bố mang lại cho bạn, bạn có con quái vật đó cho từng chức năng (và đừng quên const
và volatile
quá tải!).
Cách sử dụng chính thức của thừa kế riêng là mối quan hệ "được thực hiện theo nghĩa" (nhờ vào 'C ++ hiệu quả' của Scott Meyers cho cách diễn đạt này). Nói cách khác, giao diện bên ngoài của lớp kế thừa không có mối quan hệ (hiển thị) với lớp được kế thừa, nhưng nó sử dụng nó bên trong để thực hiện chức năng của nó.
Một cách sử dụng hữu ích của kế thừa riêng là khi bạn có một lớp thực hiện giao diện, sau đó được đăng ký với một số đối tượng khác. Bạn đặt giao diện đó ở chế độ riêng tư để lớp phải đăng ký và chỉ đối tượng cụ thể mà nó đã đăng ký mới có thể sử dụng các hàm đó.
Ví dụ:
class FooInterface
{
public:
virtual void DoSomething() = 0;
};
class FooUser
{
public:
bool RegisterFooInterface(FooInterface* aInterface);
};
class FooImplementer : private FooInterface
{
public:
explicit FooImplementer(FooUser& aUser)
{
aUser.RegisterFooInterface(this);
}
private:
virtual void DoSomething() { ... }
};
Do đó, lớp FooUser có thể gọi các phương thức riêng của FooImâyer thông qua giao diện FooInterface, trong khi các lớp bên ngoài khác không thể. Đây là một mẫu tuyệt vời để xử lý các cuộc gọi lại cụ thể được xác định là giao diện.
Tôi nghĩ phần quan trọng trong C ++ FAQ Lite là:
Việc sử dụng hợp pháp, lâu dài cho thừa kế riêng là khi bạn muốn xây dựng một lớp Fred sử dụng mã trong một lớp Wilma và mã từ lớp Wilma cần phải gọi các hàm thành viên từ lớp mới của bạn, Fred. Trong trường hợp này, Fred gọi những thứ không phải là ảo trong Wilma và Wilma gọi (thường là ảo thuần túy), được Fred ghi đè. Điều này sẽ khó hơn nhiều để làm với thành phần.
Nếu nghi ngờ, bạn nên thích sáng tác hơn thừa kế tư nhân.
Tôi thấy nó hữu ích cho các giao diện (viz. Các lớp trừu tượng) mà tôi đang kế thừa ở nơi tôi không muốn mã khác chạm vào giao diện (chỉ lớp kế thừa).
[chỉnh sửa trong một ví dụ]
Lấy ví dụ liên kết ở trên. Nói rằng
[...] Lớp Wilma cần gọi các hàm thành viên từ lớp mới của bạn, Fred.
là để nói rằng Wilma đang yêu cầu Fred có thể gọi một số chức năng thành viên nhất định, hay nói đúng hơn là Wilma là một giao diện . Do đó, như đã đề cập trong ví dụ
thừa kế tư nhân không xấu xa; Nó chỉ tốn kém hơn để duy trì, vì nó làm tăng khả năng ai đó sẽ thay đổi thứ gì đó sẽ phá vỡ mã của bạn.
nhận xét về hiệu ứng mong muốn của các lập trình viên cần đáp ứng các yêu cầu giao diện của chúng tôi hoặc phá mã. Và, vì fredCallsWilma () chỉ được bảo vệ bạn bè và các lớp dẫn xuất có thể chạm vào nó, tức là một giao diện được kế thừa (lớp trừu tượng) mà chỉ lớp kế thừa mới có thể chạm vào (và bạn bè).
[chỉnh sửa trong một ví dụ khác]
Trang này thảo luận ngắn gọn về các giao diện riêng tư (từ một góc độ khác).
Đôi khi tôi thấy hữu ích khi sử dụng thừa kế riêng tư khi tôi muốn hiển thị một giao diện nhỏ hơn (ví dụ: bộ sưu tập) trong giao diện của một giao diện khác, trong đó việc triển khai bộ sưu tập yêu cầu quyền truy cập vào trạng thái của lớp phơi bày, theo cách tương tự với các lớp bên trong Java.
class BigClass;
struct SomeCollection
{
iterator begin();
iterator end();
};
class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};
Sau đó, nếu someCollection cần truy cập BigClass, nó có thể static_cast<BigClass *>(this)
. Không cần phải có thêm một thành viên dữ liệu chiếm không gian.
BigClass
trong ví dụ này? Tôi thấy điều này thú vị, nhưng nó gào thét vào mặt tôi.
Tôi tìm thấy một ứng dụng đẹp cho thừa kế riêng tư, mặc dù nó có hạn sử dụng.
Giả sử bạn được cung cấp API C sau:
#ifdef __cplusplus
extern "C" {
#endif
typedef struct
{
/* raw owning pointer, it's C after all */
char const * name;
/* more variables that need resources
* ...
*/
} Widget;
Widget const * loadWidget();
void freeWidget(Widget const * widget);
#ifdef __cplusplus
} // end of extern "C"
#endif
Bây giờ công việc của bạn là triển khai API này bằng C ++.
Tất nhiên chúng ta có thể chọn kiểu triển khai C-ish như vậy:
Widget const * loadWidget()
{
auto result = std::make_unique<Widget>();
result->name = strdup("The Widget name");
// More similar assignments here
return result.release();
}
void freeWidget(Widget const * const widget)
{
free(result->name);
// More similar manual freeing of resources
delete widget;
}
Nhưng có một số nhược điểm:
struct
saistruct
Chúng tôi được phép sử dụng C ++, vậy tại sao không sử dụng toàn bộ sức mạnh của nó?
Các vấn đề trên về cơ bản đều gắn liền với việc quản lý tài nguyên thủ công. Giải pháp xuất hiện trong đầu là kế thừa từ Widget
và thêm một thể hiện quản lý tài nguyên vào lớp dẫn xuất WidgetImpl
cho mỗi biến:
class WidgetImpl : public Widget
{
public:
// Added bonus, Widget's members get default initialized
WidgetImpl()
: Widget()
{}
void setName(std::string newName)
{
m_nameResource = std::move(newName);
name = m_nameResource.c_str();
}
// More similar setters to follow
private:
std::string m_nameResource;
};
Điều này giúp đơn giản hóa việc thực hiện như sau:
Widget const * loadWidget()
{
auto result = std::make_unique<WidgetImpl>();
result->setName("The Widget name");
// More similar setters here
return result.release();
}
void freeWidget(Widget const * const widget)
{
// No virtual destructor in the base class, thus static_cast must be used
delete static_cast<WidgetImpl const *>(widget);
}
Như thế này, chúng tôi đã khắc phục tất cả các vấn đề trên. Nhưng một khách hàng vẫn có thể quên đi các setters WidgetImpl
và gán cho các Widget
thành viên trực tiếp.
Để gói gọn các Widget
thành viên, chúng tôi sử dụng thừa kế tư nhân. Đáng buồn thay, bây giờ chúng ta cần hai hàm bổ sung để truyền giữa cả hai lớp:
class WidgetImpl : private Widget
{
public:
WidgetImpl()
: Widget()
{}
void setName(std::string newName)
{
m_nameResource = std::move(newName);
name = m_nameResource.c_str();
}
// More similar setters to follow
Widget const * toWidget() const
{
return static_cast<Widget const *>(this);
}
static void deleteWidget(Widget const * const widget)
{
delete static_cast<WidgetImpl const *>(widget);
}
private:
std::string m_nameResource;
};
Điều này làm cho các điều chỉnh sau đây cần thiết:
Widget const * loadWidget()
{
auto widgetImpl = std::make_unique<WidgetImpl>();
widgetImpl->setName("The Widget name");
// More similar setters here
auto const result = widgetImpl->toWidget();
widgetImpl.release();
return result;
}
void freeWidget(Widget const * const widget)
{
WidgetImpl::deleteWidget(widget);
}
Giải pháp này giải quyết tất cả các vấn đề. Không quản lý bộ nhớ thủ công và Widget
được đóng gói độc đáo để WidgetImpl
không còn thành viên dữ liệu nào nữa. Nó làm cho việc thực hiện dễ sử dụng một cách chính xác và khó (không thể?) Để sử dụng sai.
Đoạn mã tạo thành một ví dụ biên dịch trên Coliru .
Nếu lớp dẫn xuất - cần sử dụng lại mã và - bạn không thể thay đổi lớp cơ sở và - đang bảo vệ các phương thức của nó bằng cách sử dụng các thành viên của cơ sở dưới khóa.
sau đó bạn nên sử dụng kế thừa riêng, nếu không bạn có nguy cơ các phương thức cơ sở đã mở khóa được xuất qua lớp dẫn xuất này.
Kế thừa riêng được sử dụng khi quan hệ không phải là "là một", nhưng lớp mới có thể được "triển khai theo thuật ngữ của lớp hiện tại" hoặc lớp mới "hoạt động như" lớp hiện có.
ví dụ từ "Tiêu chuẩn mã hóa C ++ của Andrei Alexandrescu, Herb Sutter": - Hãy xem xét rằng hai lớp Square và Hình chữ nhật đều có các hàm ảo để đặt chiều cao và chiều rộng của chúng. Sau đó, Square không thể kế thừa chính xác từ Hình chữ nhật, bởi vì mã sử dụng Hình chữ nhật có thể sửa đổi sẽ cho rằng SetWidth không thay đổi chiều cao (dù hình chữ nhật có rõ ràng là hợp đồng hay không), trong khi Square :: SetWidth không thể bảo vệ hợp đồng đó và bất biến vuông góc của chính nó tại cùng lúc. Nhưng Hình chữ nhật cũng không thể kế thừa chính xác từ Square, nếu khách hàng của Square cho rằng ví dụ diện tích của Square là bình phương chiều rộng hoặc nếu họ dựa vào một số thuộc tính khác không giữ cho Hình chữ nhật.
Hình chữ nhật "is-a" hình vuông (về mặt toán học) nhưng Hình vuông không phải là Hình chữ nhật (theo hành vi). Do đó, thay vì "is-a", chúng tôi muốn nói "works-like-a" (hoặc, nếu bạn thích, "usable-as-a") để làm cho mô tả ít bị hiểu lầm.
Một lớp giữ một bất biến. Bất biến được thiết lập bởi các nhà xây dựng. Tuy nhiên, trong nhiều trường hợp, thật hữu ích khi có chế độ xem trạng thái đại diện của đối tượng (mà bạn có thể truyền qua mạng hoặc lưu vào tệp - DTO nếu bạn muốn). REST được thực hiện tốt nhất dưới dạng AggregateType. Điều này đặc biệt đúng nếu bạn đúng. Xem xét:
struct QuadraticEquationState {
const double a;
const double b;
const double c;
// named ctors so aggregate construction is available,
// which is the default usage pattern
// add your favourite ctors - throwing, try, cps
static QuadraticEquationState read(std::istream& is);
static std::optional<QuadraticEquationState> try_read(std::istream& is);
template<typename Then, typename Else>
static std::common_type<
decltype(std::declval<Then>()(std::declval<QuadraticEquationState>()),
decltype(std::declval<Else>()())>::type // this is just then(qes) or els(qes)
if_read(std::istream& is, Then then, Else els);
};
// this works with QuadraticEquation as well by default
std::ostream& operator<<(std::ostream& os, const QuadraticEquationState& qes);
// no operator>> as we're const correct.
// we _might_ (not necessarily want) operator>> for optional<qes>
std::istream& operator>>(std::istream& is, std::optional<QuadraticEquationState>);
struct QuadraticEquationCache {
mutable std::optional<double> determinant_cache;
mutable std::optional<double> x1_cache;
mutable std::optional<double> x2_cache;
mutable std::optional<double> sum_of_x12_cache;
};
class QuadraticEquation : public QuadraticEquationState, // private if base is non-const
private QuadraticEquationCache {
public:
QuadraticEquation(QuadraticEquationState); // in general, might throw
QuadraticEquation(const double a, const double b, const double c);
QuadraticEquation(const std::string& str);
QuadraticEquation(const ExpressionTree& str); // might throw
}
Tại thời điểm này, bạn có thể chỉ lưu trữ các bộ sưu tập bộ đệm trong các thùng chứa và tra cứu nó khi xây dựng. Tiện dụng nếu có một số xử lý thực sự. Lưu ý rằng bộ đệm là một phần của QE: các thao tác được xác định trên QE có thể có nghĩa là bộ đệm có thể được sử dụng lại một phần (ví dụ: c không ảnh hưởng đến tổng); Tuy nhiên, khi không có bộ nhớ cache, đáng để tìm kiếm nó.
Kế thừa tư nhân hầu như luôn có thể được mô hình hóa bởi một thành viên (lưu trữ tham chiếu đến cơ sở nếu cần). Không phải lúc nào nó cũng xứng đáng để mô hình theo cách đó; đôi khi thừa kế là đại diện hiệu quả nhất.
Nếu bạn cần một std::ostream
vài thay đổi nhỏ (như trong câu hỏi này ), bạn có thể cần phải
MyStreambuf
xuất phát từ std::streambuf
và thực hiện các thay đổi ở đóMyOStream
xuất phát từ std::ostream
đó cũng khởi tạo và quản lý một thể hiện của MyStreambuf
và chuyển con trỏ đến thể hiện đó cho hàm tạo củastd::ostream
Ý tưởng đầu tiên có thể là thêm MyStream
cá thể làm thành viên dữ liệu vào MyOStream
lớp:
class MyOStream : public std::ostream
{
public:
MyOStream()
: std::basic_ostream{ &m_buf }
, m_buf{}
{}
private:
MyStreambuf m_buf;
};
Nhưng các lớp học cơ sở được xây dựng trước khi bất kỳ thành viên dữ liệu, do đó bạn đang đi qua một con trỏ đến một chưa xây dựng std::streambuf
ví dụ để std::ostream
đó là hành vi không xác định.
Giải pháp được đề xuất trong câu trả lời của Ben cho câu hỏi đã nói ở trên , đơn giản là kế thừa từ bộ đệm luồng trước, sau đó từ luồng và sau đó khởi tạo luồng với this
:
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Tuy nhiên, lớp kết quả cũng có thể được sử dụng như một std::streambuf
thể hiện thường không mong muốn. Chuyển sang thừa kế riêng giải quyết vấn đề này:
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Chỉ vì C ++ có một tính năng, không có nghĩa là nó hữu ích hoặc nó nên được sử dụng.
Tôi muốn nói rằng bạn không nên sử dụng nó.
Dù sao đi nữa, nếu bạn đang sử dụng nó, thì về cơ bản, bạn đang vi phạm đóng gói và hạ thấp sự gắn kết. Bạn đang đặt dữ liệu vào một lớp và thêm các phương thức thao tác dữ liệu vào một lớp khác.
Giống như các tính năng C ++ khác, nó có thể được sử dụng để đạt được các tác dụng phụ như niêm phong một lớp (như được đề cập trong câu trả lời của dribeas), nhưng điều này không làm cho nó trở thành một tính năng tốt.