C++ / a working model

09 / 163   ·   C++11   ·   7 min

class and struct: Different Default Access, Same Capabilities

Keep this sentence

In C++, both class and struct define class types, and both support constructors, destructors, inheritance, virtual functions, and templates. The language difference is mainly default member access and default inheritance access. Using struct for simple data and class to maintain invariants is a design convention, not a capability limit.

In this lesson
  1. Two defaults
  2. Object capabilities are not tiered
  3. Use convention to help maintain invariants
  4. Example
  5. Exercise

Two defaults

Members written without an access specifier default to public in a struct and to private in a class. Inheritance follows the same pattern: struct D : B is public inheritance by default, and class D : B is private inheritance by default. After you write public, protected, or private explicitly, the default difference caused by the keyword is overridden.

Default inheritance depends on the keyword used for the derived class, not on whether the base was declared with class or struct. The example gives two derived classes the same base, then uses type traits to check whether a derived pointer can convert publicly and implicitly to a base pointer. That avoids memorizing only the member default and missing the inheritance rule. The two defaults travel together: if you change the type keyword and leave access unspecified, both member visibility and inheritance access change at once. Writing the specifiers you actually want makes the class or struct keyword a matter of style rather than a hidden switch.

Object capabilities are not tiered

A struct may have private data, constructors, destructors, virtual functions, and complex business logic; a class may consist of public fields only. Seeing struct does not let you assert that there is no vtable, no construction cost, that the object can be copied bytewise, or that it can be passed directly to C. Those properties depend on the concrete members, inheritance, and special member functions.

Whether a type is an aggregate, a standard-layout type, or a trivially copyable type are three different sets of rules, and some details change with the standard version. When you design serialization or a low-level interface, check the specific properties you need instead of treating the keyword as proof of memory layout or ABI. A public-field class and a struct with a virtual destructor are both legal; only the members and bases decide layout, construction, and calling convention. Name the type for the reader, then verify the traits your ABI or wire format actually requires.

Use convention to help maintain invariants

Data such as simple coordinates or return-value bundles, where each field may be assigned independently, is usually expressed as a struct. Types that must keep relationships among fields, such as a bank balance or a resource handle, usually use class, make the state private, and centralize validation of modifications in constructors and member functions.

This is a convention for communicating design intent, not a requirement to make every field private mechanically and then add equivalent getters and setters. When choosing an interface, ask which states must always be valid and who is responsible for keeping them valid. Access control can restrict how callers write code, but it does not automatically validate numeric ranges and is not a memory-safety isolation boundary. Callers of a struct are invited to set fields independently; callers of a class with private state are invited to go through operations that preserve the invariant. Choose the invitation that matches the type, then still check ranges and resource rules in those operations.

Pitfalls

  • Changing class to struct while omitting explicit access specifiers changes both member visibility and unspecified inheritance access at the same time.
  • struct does not guarantee a C-compatible layout; reference members, virtual functions, inheritance, and members of library types all need separate review.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <iostream>
#include <type_traits>

struct Base { int value = 7; };
struct PublicChild : Base {};
class PrivateChild : Base {};

class Box {
    int value_;
public:
    explicit Box(int value) : value_(value) {}
    int value() const { return value_; }
};

int main() {
    static_assert(std::is_convertible<PublicChild*, Base*>::value, "public base");
    static_assert(!std::is_convertible<PrivateChild*, Base*>::value, "private base");
    PublicChild child;
    Box box(9);
    assert(child.value == 7);
    assert(box.value() == 9);
    std::cout << child.value << ' ' << box.value() << '\n';
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-class-struct.cpp -o example && ./example

Expected result

7 9

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If you change class Child : Base into struct Child : Base but must keep the original access policy, how should you rewrite it?

Show a reference answer

Write struct Child : private Base, and place private: before the member region that was private by default. That explicitly preserves the original default inheritance and member access; existing public: or protected: sections remain as they were. Merely changing the keyword is not a mechanical rename that preserves behavior.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog