struct или class в C++: в чём разница и что выбирать
На уровне языка struct и class в C++ различаются одной строкой стандарта. На уровне читаемости кода и договорённостей в команде — гораздо сильнее. Разбираем, где выбор реальный, а где косметический.

Формально между struct и class в C++ ровно одно различие: у struct члены и базовые классы по умолчанию публичные, у class — приватные. Всё остальное — стиль, соглашения и ожидания читателя вашего кода. Но за этой одной строкой стандарта прячется вопрос, который всплывает на каждом код-ревью: почему это тип объявлен через struct, а не class, и наоборот. Разберём, где выбор влияет на поведение, а где только на восприятие, и по какому критерию решать.
Ориентируемся на современный C++ (C++17 и новее, стандарт C++20 добавил концепты и std::is_aggregate, но базовая семантика struct/class не менялась с C++98). Всё, что ниже, компилируется любым актуальным GCC, Clang или MSVC.
Что на самом деле различает язык
Разница ровно в одном — уровень доступа по умолчанию. Эти два фрагмента дают идентичный по поведению тип:
// C++17. struct: члены публичны по умолчанию
struct Point {
int x;
int y;
};
// то же самое, но явно через class
class Point {
public:
int x;
int y;
};Различие в наследовании работает так же. struct Derived : Base означает публичное наследование, class Derived : Base — приватное. Это единственное место, где забытое ключевое слово доступа меняет смысл программы, а не только её читаемость.
Компилятор не отличает struct от class ни в одном другом аспекте. Виртуальные функции, конструкторы, деструкторы, шаблоны, дружественные функции — всё работает одинаково в обоих. Выбор — это сообщение читателю, а не инструкция машине.
Мелочь, которая ломает сборку
Есть один практический подводный камень. Если тип объявлен как struct, а где-то заранее объявлен через class Point; (или наоборот), MSVC выдаёт предупреждение C4099 и в ряде конфигураций — ошибку компоновки, потому что декорированное имя может отличаться. GCC и Clang к этому терпимее и обычно просто предупреждают. Вывод простой: объявляйте тип везде одним и тем же ключевым словом.
Агрегаты: где struct даёт то, чего не даёт привычка
Есть категория типов, где struct — не стиль, а рабочий инструмент: агрегаты1. Агрегат — это тип без пользовательских конструкторов, без приватных нестатических членов, без виртуальных функций и (в C++17) без базовых классов. Для такого типа доступна агрегатная инициализация фигурными скобками:
// C++17
struct Config {
int timeout_ms;
bool verbose;
const char* endpoint;
};
// агрегатная инициализация: поля по порядку
Config cfg{5000, true, "https://api.example.com"};
// C++20 добавил designated initializers — по именам полей
Config cfg2{.timeout_ms = 5000, .verbose = true, .endpoint = "https://api.example.com"};Как только вы добавите в Config хоть один приватный член или пользовательский конструктор, тип перестанет быть агрегатом, и первая форма инициализации перестанет компилироваться. Именно поэтому пассивные контейнеры данных — DTO, конфиги, координаты, результаты парсинга — почти всегда объявляют через struct: они и задуманы как открытые наборы полей.
Когда что выбирать
Практическое правило, которого придерживаются большинство стилевых гайдов (в том числе Google C++ Style Guide и Core Guidelines): выбор отражает намерение типа.
- struct — когда тип это просто набор данных, поля которого читаются и пишутся напрямую, а инвариантов между ними нет. Никакой логики, кроме, может быть, тривиальных методов вроде оператора сравнения.
- class — когда у типа есть инвариант, который нужно защищать: поля приватны, доступ идёт через методы, состояние согласовано. Как только появляется приватная секция и содержательные методы — это class.
Граница проходит по вопросу: есть ли между полями связь, которую можно нарушить извне. У struct Point{int x, y;} связи нет — любые x и y валидны. У класса BankAccount баланс и история операций должны быть согласованы, поэтому поля прячут за методами.
Таблица: как принять решение
| Признак типа | struct | class |
|---|---|---|
| Поля читаются/пишутся напрямую | да | нет, через методы |
| Есть инвариант между полями | нет | да |
| Нужна агрегатная инициализация {} | да, если тип остаётся агрегатом | обычно неприменимо |
| Есть содержательная логика, состояние | нет | да |
| Пример | Point, Config, ParseResult | Socket, Parser, Account |
Что ломает договорённость
Соглашение полезно ровно до тех пор, пока команда его держит. Типичные места, где оно рассыпается:
- struct с приватной секцией и десятком методов. Формально работает, но читатель ожидал открытый контейнер данных, а получил класс, притворяющийся структурой. Если появляется
private:и логика — переименуйте вclass. - class с одними публичными полями. Обратная ошибка: тип-контейнер объявлен через
classсpublic:в первой строке. Работает, но лишнийpublic:— сигнал, что здесь напрашивалсяstruct. - Смешанное объявление и определение.
struct Foo;в одном заголовке иclass Foo {...}в другом — источник предупреждений и, на MSVC, потенциальных проблем компоновки. Держите ключевое слово единым. - Наследование по невнимательности. Забытое ключевое слово доступа в
class Derived : Baseдаёт приватное наследование — часто не то, что имели в виду. Здесь разница междуstructиclassреально меняет поведение, поэтому доступ при наследовании лучше писать явно:class Derived : public Base.
Шаблоны: одно место, где ключевые слова взаимозаменяемы почти всегда
В объявлении параметра шаблона template<typename T> и template<class T> означают буквально одно и то же — здесь class не про структуры-классы, а про «тип-параметр». Выбор между ними чисто стилистический. Единственное исключение из истории: до C++17 template template-параметры требовали class, но в актуальных стандартах и там разрешён typename.
Что делать дальше
Договоритесь в команде об одном правиле и закрепите его в стилевом документе: struct — для пассивных данных без инвариантов, class — для всего с приватным состоянием. Дальше это должен проверять не человек на ревью, а инструмент. Настройте clang-tidy и линтер под ваш стиль, а форматирование отдайте clang-format, чтобы споры о struct/class не всплывали на каждом pull request. Если пишете API-заголовки для внешних потребителей — прогоняйте их сборку на всех целевых компиляторах, потому что именно расхождение struct/class в forward-объявлениях ловится только на MSVC.
1 Агрегат — тип, для которого разрешена инициализация списком в фигурных скобках без вызова пользовательского конструктора. Точный набор требований менялся между C++11, C++17 и C++20; проверить принадлежность типа к агрегатам во время компиляции можно через std::is_aggregate_v<T> (доступно с C++17).
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: cppreference: Classes (struct/class, aggregate initialization), C++ Core Guidelines: C.2, C.8 — when to use struct vs class
Частые вопросы
Есть ли разница в производительности между struct и class?
Можно ли наследовать class от struct и наоборот?
А если у меня старый стандарт, скажем C++11 — что-то из этого не сработает?
Почему MSVC ругается, а GCC компилирует то же самое молча?
Стоит ли переписывать существующий код ради этого правила?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.