Искусство Построения Архитектуры Проекта
Глава 2: Препроцессор
Препроцессор как квантовая физика: "Если вы понимаете препроцессор, значит вы его не понимаете"
Его легко понять, но в то же время нужно учитывать множество вещей. Но начнем с основ. Что делает этот ваш препроцессор ? По сути: Он преобразует удобный для человека код в форму, понятную компилятору. Наверняка вы слышали такие директивы как: #include, #define, #if, #else, #endif, #ifdef, #pragma once, #pragma region/endregion и другие...
Результатом препроцессора является Единица Трансляции (TU - Translation Unit) - это тот-же исходный код .cpp файла, но со всеми включенными заголовочными файлами (#include), раскрытыми макросами (#define) и обработанными условиями (#if, #else, #endif, #ifdef). Получившийся текст передается компилятору.
Препроцессор запускается перед компиляцией кода, и с ним вполне удобно работать. Расскажу про базовые директивы, с которыми лично я имею дело:
-
Что видит человек:
Что видит компилятор:
-
Что видит человек:
Что видит компилятор:
-
-
Про #pragma once поговорим в следующей главе
Главная изюминка препроцессора: Он никак не входит в бинарник и не добавляет лишнего веса (если только вы сами не создадите макросы, которые генерируют дополнительный код), а также он больше ориентирован (имеет большой арсенал макросов) для получение информации с системы, компилятора, окружения и тд.
Так, например, можно узнать информацию о:
- Компиляторе - MSVC, MinGW, GCC и другие
- ОС - Windows, Linux, macOS и тд.
- Какой тип сборки - Debug, Release
А в контексте Unreal Engine:
- Какая целевая сборка проекта - Development, DebugGame, Shipping
На этом теоретическая часть подходит к концу. Хотел сделать главу 3 про единицы трансляции, но она такая мелкая, что легче ее здесь расписать. В следующей главе уже поговорим про Forward-Declaration и заголовочные (.h) файлы
See the Code Before the Compiler. Think Cold.
Глава 2: Препроцессор
Препроцессор как квантовая физика: "Если вы понимаете препроцессор, значит вы его не понимаете"
Его легко понять, но в то же время нужно учитывать множество вещей. Но начнем с основ. Что делает этот ваш препроцессор ? По сути: Он преобразует удобный для человека код в форму, понятную компилятору. Наверняка вы слышали такие директивы как: #include, #define, #if, #else, #endif, #ifdef, #pragma once, #pragma region/endregion и другие...
Результатом препроцессора является Единица Трансляции (TU - Translation Unit) - это тот-же исходный код .cpp файла, но со всеми включенными заголовочными файлами (#include), раскрытыми макросами (#define) и обработанными условиями (#if, #else, #endif, #ifdef). Получившийся текст передается компилятору.
Препроцессор запускается перед компиляцией кода, и с ним вполне удобно работать. Расскажу про базовые директивы, с которыми лично я имею дело:
-
#include "Path" - Берет файл из этого пути, просто копирует его содержимое и вставляет его вместо себя. То есть буквально вставляет содержимое одного файла в другой (до компиляции). Пример:Что видит человек:
// main.cpp
#include "MathUtils.h"
int main() {
int a = 5, b = 3;
int sum = Add(a, b);
return sum;
}
// MathUtils.h
int Add(int x, int y) {
return x + y;
}
Что видит компилятор:
// На месте, где был #include "MathUtils.h"
int Add(int x, int y) {
return x + y;
}
// Он грубо вставляет код с файла MathUtils.h
int main() {
int a = 5, b = 3;
int sum = Add(a, b);
return sum;
}
-
#define - Те самые "макросы". По сути это слепая вставка частей кода. Традиционно используется для констант и повторяющихся блоков кода, НО, в то же время имеет непредсказуемость из-за своей специфики. Как говорится: Используете, но с осторожностью. Пример:Что видит человек:
#define PI 3.14
#define RADIUS 2.0
float Length = 2 * RADIUS * PI;
float Area = PI * RADIUS * RADIUS;
Что видит компилятор:
float Length = 2 * 2.0 * 3.14;
float Area = 3.14 * 2.0 * 2.0;
-
#if, #else, #endif, #ifdef - Условная компиляция. Позволяет включать/исключать куски кода по условию. Крайне полезная штука-
#pragma region/endregion - Не стандартные С++ директивы (Но поддерживаются в Visual Studio, Rider, CLion и другие). Сделаны чисто для визуального "сворачивания" кода в области/регионы. Опять же - созданы только для IDE, никак не влияют на компиляцию (чисто косметическая функция).Про #pragma once поговорим в следующей главе
Главная изюминка препроцессора: Он никак не входит в бинарник и не добавляет лишнего веса (если только вы сами не создадите макросы, которые генерируют дополнительный код), а также он больше ориентирован (имеет большой арсенал макросов) для получение информации с системы, компилятора, окружения и тд.
Так, например, можно узнать информацию о:
- Компиляторе - MSVC, MinGW, GCC и другие
- ОС - Windows, Linux, macOS и тд.
- Какой тип сборки - Debug, Release
А в контексте Unreal Engine:
- Какая целевая сборка проекта - Development, DebugGame, Shipping
На этом теоретическая часть подходит к концу. Хотел сделать главу 3 про единицы трансляции, но она такая мелкая, что легче ее здесь расписать. В следующей главе уже поговорим про Forward-Declaration и заголовочные (.h) файлы
See the Code Before the Compiler. Think Cold.
❤2🔥1
Искусство Построения Архитектуры Проекта
Глава 3: Заголовочные файлы и щепотка магии в Forward Declaration
Вспомним:
- Глава 0 - Для нас объявить функцию означает "Просто очерк в коде", а реализовать - "Прописать тело функции (ее внутрянку)"
- Глава 1 - Если мы в одном .cpp файле объявим функцию, а в другом реализуем, то Линкер свяжет объявление с реализацией и все будет в порядке
- Глава 2 - #include "Path/File.h" - открывает файл по пути и заменяет себя на его содержимое
По сути, если еще раз прочитать Главу 0 и Главу 1, то можете учуять тонкую взаимосвязь. Поздравляю, вы только что заметили механизм в С++ под названием Forward Declaration!
Ладно, начнем с примера и плавно раскроем тему - допустим у нас есть библиотека математики:
Теперь я хочу вызвать ее в другом .cpp файле, как это сделать? Верно, объявить функцию int Add(int a, int b)!
Что произойдет под капотом? Конечно, компилятор скомпилирует Math.cpp и main.cpp (в котором будет нереализованная функция int Add(int a, int b)), а затем Линкер найдет реализацию этой функции в Math.obj и свяжет ее, тем самым мы получим один цельный и рабочий бинарный файл. Магия! Добро пожаловать в Forward Declaration! Ибо через эту строку:
Мы неформально договариваемся с компилятором: "Да, бро, есть такая функция Add(...) и ты не знаешь ее реализацию. Но поверь, я обещаю что при линковке ты найдешь ее" - а это, по сути, сам смысл Forward Declaration в чистом виде.
Но если я хочу добавить функцию int Sub(int a, int b)? Тогда придется еще раз объявить ее в main.cpp. Хорошо.... А если таких функций будет за 200+ штук? Что тогда?
А тогда дяденька Бьёрн Страуструп подумал и добавил такое понятие как Заголовочный файл с расширением .h в котором он соберет все Forward Declaration в один пучок. Разберем на практике:
Создадим заголовочный файл:
И теперь в Math.cpp мы можем реализовать функцию:
А в main.cpp просто воспользоваться ею:
И по сути при препроцессинге, при раскрытии #include "Math.h" препроцессором мы просто получим Forward Declaration функции int Add(int a, int b) в начале .cpp файлов что, собственно говоря, нам и нужно было ¯\_(ツ)_/¯
Кстати, в Math.cpp при раскрытии препроцессором получится так, что сначала мы объявляем функцию Add(...), а через пару строк ниже реализуем ее. В принципе - так делать можно, Америку мы не открываем, вселенная не схлопнится :D
Какие плюсы? Например теперь чтобы вызвать/реализовать функцию достаточно подключить .h файл через #include, а затем препроцессор сделает всю работу сам. Красотень же ведь! Сила плюсов :D
Интересный факт: Еще вы можете встретить два фундаментальных формата заголовков - .h и .hpp. Изначально .hpp задумывался как способ отделить C++'шные заголовки от C'шных, но сегодня разница скорее культурная: .h - традиционный выбор, особенно в больших проектах (Unreal Engine, Microsoft). .hpp - выбор популярных C++ библиотек и новых проектов. Так что технической разницы нет - оба работают одинаково. Тут наверное философский вопрос :D
Теперь вы частично знаете магию плюсов и основную механику разделения кода на .h / .cpp файлы - Поздравляю, ибо большинство новичков спотыкаются на этом (даже я в свое время-), но поняв как это работает по капотом, осознаешь всю простоту и мощь плюсов! В следующей главе поговорим про цикличность подключаемых заголовочных файлов и первые способы оптимизации через Forward Declaration.
Declare Now - Implement Later. Think Cold.
Глава 3: Заголовочные файлы и щепотка магии в Forward Declaration
Вспомним:
- Глава 0 - Для нас объявить функцию означает "Просто очерк в коде", а реализовать - "Прописать тело функции (ее внутрянку)"
- Глава 1 - Если мы в одном .cpp файле объявим функцию, а в другом реализуем, то Линкер свяжет объявление с реализацией и все будет в порядке
- Глава 2 - #include "Path/File.h" - открывает файл по пути и заменяет себя на его содержимое
По сути, если еще раз прочитать Главу 0 и Главу 1, то можете учуять тонкую взаимосвязь. Поздравляю, вы только что заметили механизм в С++ под названием Forward Declaration!
Ладно, начнем с примера и плавно раскроем тему - допустим у нас есть библиотека математики:
// Math.cpp
int Add(int a, int b) {
return a + b;
}
Теперь я хочу вызвать ее в другом .cpp файле, как это сделать? Верно, объявить функцию int Add(int a, int b)!
// main.cpp
int Add(int a, int b);
// Только объявили функцию
// Реализация в другом файле
int main() {
int sum = Add(2, 3);
return 0;
}
Что произойдет под капотом? Конечно, компилятор скомпилирует Math.cpp и main.cpp (в котором будет нереализованная функция int Add(int a, int b)), а затем Линкер найдет реализацию этой функции в Math.obj и свяжет ее, тем самым мы получим один цельный и рабочий бинарный файл. Магия! Добро пожаловать в Forward Declaration! Ибо через эту строку:
int Add(int a, int b);
Мы неформально договариваемся с компилятором: "Да, бро, есть такая функция Add(...) и ты не знаешь ее реализацию. Но поверь, я обещаю что при линковке ты найдешь ее" - а это, по сути, сам смысл Forward Declaration в чистом виде.
Но если я хочу добавить функцию int Sub(int a, int b)? Тогда придется еще раз объявить ее в main.cpp. Хорошо.... А если таких функций будет за 200+ штук? Что тогда?
А тогда дяденька Бьёрн Страуструп подумал и добавил такое понятие как Заголовочный файл с расширением .h в котором он соберет все Forward Declaration в один пучок. Разберем на практике:
Создадим заголовочный файл:
// Math.h
int Add(int a, int b);
// Лишь объявили функцию!
И теперь в Math.cpp мы можем реализовать функцию:
// Math.cpp
#include "Math.h"
int Add(int a, int b) {
return a + b;
}
А в main.cpp просто воспользоваться ею:
// main.cpp
#include "Math.h"
int main() {
int sum = Add(2, 3);
return 0;
}
И по сути при препроцессинге, при раскрытии #include "Math.h" препроцессором мы просто получим Forward Declaration функции int Add(int a, int b) в начале .cpp файлов что, собственно говоря, нам и нужно было ¯\_(ツ)_/¯
Кстати, в Math.cpp при раскрытии препроцессором получится так, что сначала мы объявляем функцию Add(...), а через пару строк ниже реализуем ее. В принципе - так делать можно, Америку мы не открываем, вселенная не схлопнится :D
Какие плюсы? Например теперь чтобы вызвать/реализовать функцию достаточно подключить .h файл через #include, а затем препроцессор сделает всю работу сам. Красотень же ведь! Сила плюсов :D
Интересный факт: Еще вы можете встретить два фундаментальных формата заголовков - .h и .hpp. Изначально .hpp задумывался как способ отделить C++'шные заголовки от C'шных, но сегодня разница скорее культурная: .h - традиционный выбор, особенно в больших проектах (Unreal Engine, Microsoft). .hpp - выбор популярных C++ библиотек и новых проектов. Так что технической разницы нет - оба работают одинаково. Тут наверное философский вопрос :D
Теперь вы частично знаете магию плюсов и основную механику разделения кода на .h / .cpp файлы - Поздравляю, ибо большинство новичков спотыкаются на этом (даже я в свое время-), но поняв как это работает по капотом, осознаешь всю простоту и мощь плюсов! В следующей главе поговорим про цикличность подключаемых заголовочных файлов и первые способы оптимизации через Forward Declaration.
Declare Now - Implement Later. Think Cold.
❤2🔥1💯1
Искусство Построения Архитектуры Проекта
Глава 4: "Квантовая запутанность" классов и "теория большого #includ'а"
Вспомним:
- Глава 2 - #include "Path/File.h" - открывает файл по пути и жестко заменяет себя на его содержимое
- Глава 3 - Forward Declaration в связке с .h файлом позволяет в других файлах только объявить функцию, а в одном конкретном .cpp файле реализовать ее. Затем линкер совместит все нереализованные функции с их реализациями (Глава 0)
Не буду душнить теорией, начнем сразу с кейса: У нас есть класс Player и World которые взаимосвязаны - игрок может получить мир в котором находится, мир может узнать что за игрок в нем живет. Составим код:
Но если мы запустим компиляцию, то получим ошибку, ведь формально компилятор не знает что такое World в классе игрока и Player в классе мира аналогично
Какова идея? Вспомним с Главы 2, что мы можем подключить .h файл в котором будут объявлены классы/функции. Сделаем:
^ И вот тут мы получаем уроборос .h файлов, ибо взглянем это с препроцессора:
↓ Он открывает Player.h
↓ Внутри Player.h есть #include "World.h"
↓ Он открывает World.h, заменяет строку на его содержимое
↓ Внутри World.h есть #include "Player.h"
↓ Он открывает Player.h, заменяет строку на его содержимое
↓ Внутри Player.h есть #include "World.h"
↓ Он открывает World.h, заменяет строку на его содержимое
↓ Внутри World.h есть #include "Player.h"...
↓ ...
Безумие, не так ли? В этом и суть Циклической зависимости (Circular Dependency) которую мы только что открыли! Но и у этого есть пару решений
Еще в Главе 2 я упомянул про #pragma once - что ж, это его звездный час! Ибо #pragma once так и говорит препроцессору - этот .h файл подключать ТОЛЬКО 1 РАЗ! Не более! И обычно эта директива пишется в начале каждого .h файла, то есть:
И вот теперь при препроцессинге в файле Player.h подключится/раскроется World.h и на этом цикл будет оборван из-за #pragma once вначале Player.h - магия! Но мы лишь остановили признак (рекурсию), но не решили проблему. Взглянем на Player.h после препроцессинга:
Но подождите-ка, как класс World должен знать об Player, если он объявлен чуть ниже? Мда.. И тут неожиданно на помощь приходит Forward Declaration с прошлой главы, ну конечно же! Если мы вначале просто объявим/упомянем что есть такой-то класс/функция, то при линковке они найдут друг-друга, поздравляю! Это и было решением и оптимизацией в тоже время:
Но подождите, почему именно оптимизацией? По опыту: #include крайне грубая штука - он раскрывает все файлы рекурсивно и если, например, в проекте есть 1 .h файл который имеет 20 #includ'ов внутри, то при использовании этого .h вместе с ним будут раскрываться И ТЕ 20 .h ФАЙЛОВ! То есть да: #include - удобный, но имеет накопительный эффект. И именно поэтому использование Forward Declaration лучшее решение из возможных
На этом пожалуй все. Вышло громоздко, но благо мы уже плавно подбираемся к основной теме - проектирование архитектуры. В следующей небольшой главе поговорим про понятие inline функций и жирную проблему с их реализацией внутри .h файлов (Об этом спотыкаются многие, даже мои знакомые - главное понять эту тему и вместе сделать выводы)
Break The Chain. Think Cold.
Глава 4: "Квантовая запутанность" классов и "теория большого #includ'а"
Вспомним:
- Глава 2 - #include "Path/File.h" - открывает файл по пути и жестко заменяет себя на его содержимое
- Глава 3 - Forward Declaration в связке с .h файлом позволяет в других файлах только объявить функцию, а в одном конкретном .cpp файле реализовать ее. Затем линкер совместит все нереализованные функции с их реализациями (Глава 0)
Не буду душнить теорией, начнем сразу с кейса: У нас есть класс Player и World которые взаимосвязаны - игрок может получить мир в котором находится, мир может узнать что за игрок в нем живет. Составим код:
// Player.h
class Player {
World* World;
}
// World.h
class World {
Player* Player;
}
Но если мы запустим компиляцию, то получим ошибку, ведь формально компилятор не знает что такое World в классе игрока и Player в классе мира аналогично
Какова идея? Вспомним с Главы 2, что мы можем подключить .h файл в котором будут объявлены классы/функции. Сделаем:
// Player.h
#include "World.h"
class Player {
World* World;
};
// World.h
#include "Player.h"
class World {
Player* Player;
};
^ И вот тут мы получаем уроборос .h файлов, ибо взглянем это с препроцессора:
↓ Он открывает Player.h
↓ Внутри Player.h есть #include "World.h"
↓ Он открывает World.h, заменяет строку на его содержимое
↓ Внутри World.h есть #include "Player.h"
↓ Он открывает Player.h, заменяет строку на его содержимое
↓ Внутри Player.h есть #include "World.h"
↓ Он открывает World.h, заменяет строку на его содержимое
↓ Внутри World.h есть #include "Player.h"...
↓ ...
Безумие, не так ли? В этом и суть Циклической зависимости (Circular Dependency) которую мы только что открыли! Но и у этого есть пару решений
Еще в Главе 2 я упомянул про #pragma once - что ж, это его звездный час! Ибо #pragma once так и говорит препроцессору - этот .h файл подключать ТОЛЬКО 1 РАЗ! Не более! И обычно эта директива пишется в начале каждого .h файла, то есть:
// Player.h
#pragma once
#include "World.h"
class Player {
World* World;
};
// World.h
#pragma once
#include "Player.h"
class World {
Player* Player;
};
И вот теперь при препроцессинге в файле Player.h подключится/раскроется World.h и на этом цикл будет оборван из-за #pragma once вначале Player.h - магия! Но мы лишь остановили признак (рекурсию), но не решили проблему. Взглянем на Player.h после препроцессинга:
// Player_AfterPreprocessing.h
#pragma once
// #include "World.h"
// ↓ - Начало "World.h"
#pragma once
// #include "Player.h"
// Блокируется из-за #pragma once
// В начале файла "Player.h"
class World {
Player* Player; // ?
};
// ↑ - Конец "World.h"
// Остальной код
class Player {
World* World;
};
Но подождите-ка, как класс World должен знать об Player, если он объявлен чуть ниже? Мда.. И тут неожиданно на помощь приходит Forward Declaration с прошлой главы, ну конечно же! Если мы вначале просто объявим/упомянем что есть такой-то класс/функция, то при линковке они найдут друг-друга, поздравляю! Это и было решением и оптимизацией в тоже время:
// Player.h
#pragma once
class World;
// Forward Declaration
class Player {
World* World;
};
// World.h
#pragma once
class Player;
// Forward Declaration too
class World {
Player* Player;
};
Но подождите, почему именно оптимизацией? По опыту: #include крайне грубая штука - он раскрывает все файлы рекурсивно и если, например, в проекте есть 1 .h файл который имеет 20 #includ'ов внутри, то при использовании этого .h вместе с ним будут раскрываться И ТЕ 20 .h ФАЙЛОВ! То есть да: #include - удобный, но имеет накопительный эффект. И именно поэтому использование Forward Declaration лучшее решение из возможных
На этом пожалуй все. Вышло громоздко, но благо мы уже плавно подбираемся к основной теме - проектирование архитектуры. В следующей небольшой главе поговорим про понятие inline функций и жирную проблему с их реализацией внутри .h файлов (Об этом спотыкаются многие, даже мои знакомые - главное понять эту тему и вместе сделать выводы)
Break The Chain. Think Cold.
❤🔥2😱1
Розыгрыш багов на Альфу 1.1.0
Кто найдет -тому 10 лет условно 💀
Успей получить свой долгожданный отпуск!
GameJolt: тык
Кто найдет -
Успей получить свой долгожданный отпуск!
GameJolt: тык
❤3🤩2❤🔥1😭1😎1
Искусство Построения Архитектуры Проекта
Глава 5: inline-функции и правило одной реализации
Данная глава будет маленькой, но крайне полезной. Основной вопрос: Можно ли реализовывать функции в .h файле?
Ответ: Да, но с оговорками
Вы сделали это:
Что тогда? Ну... Во-первых компилятор вам банально не даст этого сделать - сработает намеренная защита. Представьте, что вы подключите этот Test.h файл в нескольких .cpp файлах -> Препроцессор раскроет #include "Test.h" -> В нескольких .cpp файлах будет int Add(...) {...} (прошу заметить - с реализацией!) - и когда линкер начнет сшивать .obj файлы вместе, то в каждом из них будет объявлена и реализована функция Add, и вопрос: Какую реализацию ему выбрать? Вот именно...
Так что нам придумали одно правило: Правило Одного Определения (ODR - One Definition Rule)
"А если я хочу намеренно реализовать функцию в .h файле?" - Для нас дядя Бьёрн позаботился и придумал пометку inline - это пометка компилятору, которая говорит: вставь код функции напрямую в место её вызова вместо выполнения традиционного вызова. То есть:
Что это дает? При компиляции, эта функция по факту не будет создана, а ее машинный код будет вшит в тех местах, где она вызывается. То есть это такой "своеобразный #include для функций". И главное: при той же линковке, функция Add по факту не будет создана, а значит не будет проблем с ODR
И главный вопрос: какие плюсы-минусы у этого? - Безусловно это меньше беготни процессору. Серьезно, если он меньше бегает между функциями, то это имеет оптимизирующий эффект. Вместо того, чтобы вызвать функцию и ждать ее ответа, он сразу здесь и сейчас исполняет ее.
Но и у этого есть оттягивающий эффект: Я не зря сравнил inline с #include - ибо он также имеет накопительный эффект: Чем больше функций будут раскрываться - тем крупнее будет бинарник. А также учтите ещё размер функции, и получите охерительно жирный бинарник и несколько часов компиляции. Бинго!
В любом случае, как и говорилось ранее, главное разобраться в этой теме и сделать выводы. Я не говорю не использовать эту механику. Наоборот, даже в своем проекте Portal: Solver у меня есть отдельный файл утилит (PortalSolverUtilities.h) в которых реализованы небольшие inline функции-помощники и используются при необходимости. Но реализовывать целый движок в единственном .h... Мда... Надеюсь теперь вы понимаете цену своих действий
ODR Looks Like Error - But It's Just Protection From Chaos. Think Cold.
Глава 5: inline-функции и правило одной реализации
Данная глава будет маленькой, но крайне полезной. Основной вопрос: Можно ли реализовывать функции в .h файле?
Ответ: Да, но с оговорками
Вы сделали это:
// Test.h
#pragma once
int Add(int a, int b) {
return a + b;
}
Что тогда? Ну... Во-первых компилятор вам банально не даст этого сделать - сработает намеренная защита. Представьте, что вы подключите этот Test.h файл в нескольких .cpp файлах -> Препроцессор раскроет #include "Test.h" -> В нескольких .cpp файлах будет int Add(...) {...} (прошу заметить - с реализацией!) - и когда линкер начнет сшивать .obj файлы вместе, то в каждом из них будет объявлена и реализована функция Add, и вопрос: Какую реализацию ему выбрать? Вот именно...
Так что нам придумали одно правило: Правило Одного Определения (ODR - One Definition Rule)
"А если я хочу намеренно реализовать функцию в .h файле?" - Для нас дядя Бьёрн позаботился и придумал пометку inline - это пометка компилятору, которая говорит: вставь код функции напрямую в место её вызова вместо выполнения традиционного вызова. То есть:
// Test.h
#pragma once
inline int Add(int a, int b) {
return a + b;
}
Что это дает? При компиляции, эта функция по факту не будет создана, а ее машинный код будет вшит в тех местах, где она вызывается. То есть это такой "своеобразный #include для функций". И главное: при той же линковке, функция Add по факту не будет создана, а значит не будет проблем с ODR
И главный вопрос: какие плюсы-минусы у этого? - Безусловно это меньше беготни процессору. Серьезно, если он меньше бегает между функциями, то это имеет оптимизирующий эффект. Вместо того, чтобы вызвать функцию и ждать ее ответа, он сразу здесь и сейчас исполняет ее.
Но и у этого есть оттягивающий эффект: Я не зря сравнил inline с #include - ибо он также имеет накопительный эффект: Чем больше функций будут раскрываться - тем крупнее будет бинарник. А также учтите ещё размер функции, и получите охерительно жирный бинарник и несколько часов компиляции. Бинго!
В любом случае, как и говорилось ранее, главное разобраться в этой теме и сделать выводы. Я не говорю не использовать эту механику. Наоборот, даже в своем проекте Portal: Solver у меня есть отдельный файл утилит (PortalSolverUtilities.h) в которых реализованы небольшие inline функции-помощники и используются при необходимости. Но реализовывать целый движок в единственном .h... Мда... Надеюсь теперь вы понимаете цену своих действий
ODR Looks Like Error - But It's Just Protection From Chaos. Think Cold.
🔥4❤🔥1
Есть еще главы, но, что вы хотите услышать первее?
Final Results
90%
Работа с enum - Удобная замена чисел на слова = читаемость (+ небольшие хаки от меня)
40%
Пространство имен (namespace) - Как инструмент для разделения и читаемости кода
40%
Разделение ответственности - Фундаментальный принцип в проектировании архитектуры
❤1❤🔥1🤩1
Portal: Solver с открытым исходным кодом?!
ДА! Вы не ослышались - Portal Solver теперь open-source!
Но чтобы вы меньше тратили время на билд проекта (почти 1.5к ассетов, я не шучу 💀)
Я решил скомпилировать все за вас! Так что успейте опробовать скомпилированные исходники Портал Солвера БЕСПЛАТНО!
Ссылочка: тык
ДА! Вы не ослышались - Portal Solver теперь open-source!
Но чтобы вы меньше тратили время на билд проекта (почти 1.5к ассетов, я не шучу 💀)
Я решил скомпилировать все за вас! Так что успейте опробовать скомпилированные исходники Портал Солвера БЕСПЛАТНО!
Ссылочка: тык
🤩3👀1
𝐑𝐨𝐨𝐭𝐓𝐨𝐨𝐥 𝐁𝐥𝐨𝐠
Искусство Построения Архитектуры Проекта Глава 4: "Квантовая запутанность" классов и "теория большого #includ'а" Вспомним: - Глава 2 - #include "Path/File.h" - открывает файл по пути и жестко заменяет себя на его содержимое - Глава 3 - Forward Declaration…
Forward Declaration на реальном примере
Как всегда работал (да.. в 2:25 ночи) и встретил распространенную/привычную проблему - в одной структуре я ссылаюсь (упоминаю) другую структуру, что находится чуть ниже
Что делать? Forward Declaration!
вот так примеры с учебника реально встречаются на работе))
Don’t Just Learn - Understand It. Think Cold
Как всегда работал (да.. в 2:25 ночи) и встретил распространенную/привычную проблему - в одной структуре я ссылаюсь (упоминаю) другую структуру, что находится чуть ниже
Что делать? Forward Declaration!
вот так примеры с учебника реально встречаются на работе))
Don’t Just Learn - Understand It. Think Cold
❤3👍1🔥1
Что ж, взял себе наконец-таки новый (второй) блокнот для Portal: Solver
А что с первым? А первый я взял ещё в прошлом году, когда только Альфа начиналась...
И сейчас он весь исписан расчётами, концептами камер, архитектур кода, систем/подсистем, и вообще темный лес☠️
Теперь, как говорится, начнем с чистого листа
А что с первым? А первый я взял ещё в прошлом году, когда только Альфа начиналась...
И сейчас он весь исписан расчётами, концептами камер, архитектур кода, систем/подсистем, и вообще темный лес☠️
Теперь, как говорится, начнем с чистого листа
❤2💯2🔥1
Искусство Построения Архитектуры Проекта
Глава 6: Enumerating the world!
Enum, enum, енум... что такое этот ваш enum? По сути - более красивая замена чисел на понятные слова.
Его синтаксис прост до безобразия:
Что можно сразу выделить:
- EnumName - название нашего перечисления
- EnumType - тип перечисления. По дефолту это int, но могут быть любые другие целочисленные типы (поговорим чуть позже)
Теперь можно обращаться к перечислению как: if(EnumName::A == EnumName::B);, или, EnumName MyEnumVar = EnumName::A;
Прикольно, да? Но что находится под enum? При компиляции компилятор просто идёт по элементам сверху вниз. Если значение у элемента не указано - берёт значение предыдущего и прибавляет к нему +1 (И да, по умолчанию первый элемент равен 0) То есть:
Но каков предел и размерность значений? А вот именно тут ключевую роль играет EnumType (по умолчанию - int). Но можно, например, enum class EnumName : uint8_t - 8 битное целое число без знака (от 0 до 255)
Но плюсы на то плюсы, что их гибкость позволяет задавать элементам любые значения. Разберем конкретный кейс:
И теперь при компиляции, код OK примет значение 200, NotFound - 404, а Something (как мы сказали выше) будет равен 405, так как компилятор даст ему значение относительно значения предыдущего элемента, да +1 (404 + 1 = 405)
А благодаря удобству enum, теперь мы можем создавать функции ECode GetCode(); и возвращать значения с помощью return ECode::OK; - удобно? Ещё как! И это одна из причин, почему enum активно используют в архитектуре
Да, конечно вышло суховато... но примите базу. В следующей главе более откровенно поговорим про хитрости использования enum
Enumerate The World. Think Cold.
Глава 6: Enumerating the world!
Enum, enum, енум... что такое этот ваш enum? По сути - более красивая замена чисел на понятные слова.
Его синтаксис прост до безобразия:
enum class EnumName : EnumType
{
A, B, C
};
Что можно сразу выделить:
- EnumName - название нашего перечисления
- EnumType - тип перечисления. По дефолту это int, но могут быть любые другие целочисленные типы (поговорим чуть позже)
Теперь можно обращаться к перечислению как: if(EnumName::A == EnumName::B);, или, EnumName MyEnumVar = EnumName::A;
Прикольно, да? Но что находится под enum? При компиляции компилятор просто идёт по элементам сверху вниз. Если значение у элемента не указано - берёт значение предыдущего и прибавляет к нему +1 (И да, по умолчанию первый элемент равен 0) То есть:
enum class EnumName : EnumType
{
// Начинаем счет с нуля
A, // 0
B, // 1
C // 2
};
Но каков предел и размерность значений? А вот именно тут ключевую роль играет EnumType (по умолчанию - int). Но можно, например, enum class EnumName : uint8_t - 8 битное целое число без знака (от 0 до 255)
Но плюсы на то плюсы, что их гибкость позволяет задавать элементам любые значения. Разберем конкретный кейс:
enum class ECode // int
{
OK = 200,
NotFound = 404,
Something // 405
};
И теперь при компиляции, код OK примет значение 200, NotFound - 404, а Something (как мы сказали выше) будет равен 405, так как компилятор даст ему значение относительно значения предыдущего элемента, да +1 (404 + 1 = 405)
А благодаря удобству enum, теперь мы можем создавать функции ECode GetCode(); и возвращать значения с помощью return ECode::OK; - удобно? Ещё как! И это одна из причин, почему enum активно используют в архитектуре
Да, конечно вышло суховато... но примите базу. В следующей главе более откровенно поговорим про хитрости использования enum
Enumerate The World. Think Cold.
❤4❤🔥1💯1