Part II — The Language, Side by Side¶
Chapter 4 — Classes, Inheritance, Interfaces¶
The mechanics you already know, in C++ spelling — plus the parts C# doesn't have at all.
Class anatomy — the differences at a glance¶
class Widget : public Shape { // 'public' inheritance - see below
public: // access specifiers are SECTIONS,
Widget(); // not per-member keywords
Widget(int size, std::string name);
int GetSize() const { return size_; } // no properties in C++ -
void SetSize(int s) { size_ = s; } // getters/setters by hand
protected:
void Recalc();
private:
int size_ = 0; // default member initializers (C++11)
std::string name_;
static int count_; // declared here, DEFINED in the .cpp:
}; // <-- the semicolon! forget it and enjoy
// a cascade of bizarre errors
// Widget.cpp
int Widget::count_ = 0; // static member definition
Widget::Widget() : Widget(0, "unnamed") {} // delegating ctor (C++11)
(That trailing const on GetSize — a promise the call changes nothing — is Appendix A.5's subject, along with const parameters.)
Key deltas from C#: access specifiers label whole sections rather than each member; there are no properties (write Get/Set methods — no get; set; sugar); static data members need a separate definition in a .cpp (pre-C++17; inline static fixes it now); and the closing brace takes a semicolon — the classic returning-developer stumble.
Constructors and the member initializer list¶
class Widget {
const int id_; // const member - CANNOT be assigned in the body
std::string name_;
Shape& canvas_; // reference member - same, must be initialized
public:
Widget(int id, std::string name, Shape& canvas)
: id_(id), // initializer list: members are
name_(std::move(name)), // CONSTRUCTED here, directly
canvas_(canvas)
{
// body runs AFTER all members are already constructed.
// name_ = name; here = construct empty, then assign - wasted work
}
};
In C# assigning fields in the constructor body is normal. In C++ the member initializer list is the proper way: members are constructed once, directly, in declaration order (not list order — a compiler warning and a common surprise). const and reference members can only be initialized — here, or by a default member initializer in the class body — never assigned in the constructor body.
Calling the base class¶
class Circle : public Shape {
public:
Circle(int x, int y, int r)
: Shape(x, y), // base ctor call - in the initializer list,
radius_(r) {} // not 'base(x, y)' like C#
void Draw() override {
Shape::Draw(); // no 'base.' keyword - name the class
DrawOutline(); // explicitly: ClassName::Member
}
private:
int radius_;
};
Inheritance access — public / protected / private¶
C# has one kind of inheritance. C++ has three; the keyword before the base name sets a ceiling on inherited member visibility:
class Circle : public Shape { }; // "is-a" - what C# does. Use this 99%.
class Circle : protected Shape { }; // inherited publics become protected
class Circle : private Shape { }; // "implemented-in-terms-of" - outsiders
// can't even treat Circle as a Shape
Trap: For 'class' the DEFAULT is private inheritance — writing class Circle : Shape silently breaks polymorphism (Shape* p = &circle; won't compile). Always write public explicitly.
Interfaces — no keyword, just a convention¶
// C#: interface IDrawable { void Draw(); }
// C++: an abstract class with only pure virtuals and a virtual dtor
class IDrawable {
public:
virtual ~IDrawable() = default; // Chapter 5: always!
virtual void Draw() = 0; // = 0 -> pure virtual -> "abstract"
virtual bool IsVisible() const { return true; } // default impl allowed
};
class Widget : public IDrawable, public ISerializable { // multiple bases OK
public:
void Draw() override;
void Serialize(Stream& s) override;
};
Multiple inheritance and the diamond¶
C# forbids multiple base classes; C++ allows them — which is exactly how it does "implementing multiple interfaces". Full multiple inheritance of classes with data brings the famous diamond problem:
struct Device { int id_; }; // struct: a plain data bag (see below)
class Scanner : public Device { };
class Printer : public Device { };
class Copier : public Scanner, public Printer { };
// Copier now contains TWO Device subobjects; copier.id_ is ambiguous!
// The fix - virtual inheritance (one shared Device):
class Scanner : virtual public Device { };
class Printer : virtual public Device { };
Try it (30 seconds). Type the broken diamond in and ask for copier.id_: the complaint names both Device subobjects. Predict what happens to that line under the virtual-inheritance fix before rebuilding.
The stance to hold: "I keep multiple inheritance to interface-style bases — pure virtual, no data — which defuses the worst of the diamond: no duplicated state. Two interfaces sharing an ancestor still leave an ambiguous upcast the compiler flags; virtual inheritance resolves it, and I treat needing that as a design smell."
Odds and ends worth 10 seconds each¶
struct Point { double x, y; }; // struct == class, just public by default;
// convention: struct for plain data bags
class Widget final { }; // 'final' = C# sealed (also per-method)
friend class Serializer; // 'friend': grants ANOTHER class/function
// access to privates. No C# equivalent
// (closest: internal). Use sparingly.
// No universal root: C++ classes do NOT inherit from anything by default.
// There is no Object, no ToString/Equals/GetHashCode for free.
// 'this' is a POINTER (this->size_), not a reference like C#'s this.
| Feature | C# | C++ |
|---|---|---|
| Interfaces | interface keyword | abstract class, all pure virtual |
| Multiple base classes | no (interfaces only) | yes — use for interfaces; beware diamond |
| Properties | get; set; sugar | hand-written Get/Set methods |
| Base access | base.Method() | ClassName::Method() |
| Base ctor call | : base(args) | : BaseName(args) in init list |
| Seal a class | sealed | final |
| Universal root | System.Object | none — no default ToString/Equals |
| Field init | assign in ctor body | member initializer list (required for const/refs) |
| Default inheritance | always public | private for class! Write 'public' explicitly |
In Rust¶
There is no inheritance. A type is a struct (or an enum) plus impl blocks, and an interface is a trait — no keyword to mark it as one, exactly as here, but a trait is all that can be implemented by another type, so the diamond cannot be built. Runtime polymorphism is Box<dyn Trait>, which is unique_ptr<Base> with the vtable pointer kept beside the object rather than inside it; the compile-time kind is a generic with a trait bound (Chapter 7). The member-initializer-list rules have no counterpart because there is no two-step construction: a struct literal names every field at once and an unnamed field is an error, so a member cannot be read before it is set and "which order do fields initialize in" is a question about Drop only (declaration order, like here). There is no protected, and private is the default — a module boundary, not a class one.
In the wild: C-style SDKs¶
C++-side SDK layers use these patterns heavily: vendor base classes, interface-style pure virtual classes for observers and callbacks, and the occasional 'friend' in container internals. Your own model layer on top of any SDK is where you apply this — interface bases behind unique_ptr, always public inheritance, always virtual destructors.