Код учебного плагина (plug-in)плагин (plug-in) — Программный модуль эффекта или инструмента, который загружается в DAW или другую программу-хост и обрабатывает переданные ему аудиоблоки. тремоло (tremolo)тремоло (tremolo) — Эффект периодического изменения амплитуды аудиосигнала. Скорость пульсации задаёт частота управляющего генератора, а диапазон усиления зависит от глубины и способа её преобразования. из официального курса JUCE написан на C++23 и местами выглядит непривычно: макросы вместо констант, пустые функции для «использования» параметра, static_cast на каждом шагу. Эта статья разбирает язык ровно в том объёме, который нужен, чтобы читать этот код и дописывать его. Каждое понятие показано на строке из этого плагина, а за подробностями ведут заметки на полях со ссылками на учебник Learn C++ и справочник cppreference.
Фрагменты взяты из готовой версии плагина в папке complete репозитория tremolo-juce-course (JUCE 8.0.12). Короткие учебные примеры без JUCE проверены компиляцией: Apple clang 17, режим C++23.
C++ развивается стандартами: C++98, C++11, C++14, C++17, C++20, C++23. Рядом с возможностями, которые появились после 1998 года, в скобках указан стандарт. Плагин собирается в режиме C++23, поэтому доступно всё перечисленное.
Классы, конструкторы, наследование, шаблоны и время жизни объектов (object lifetime)время жизни объекта (object lifetime) — Промежуток от окончания конструктора до начала деструктора, в который к объекту можно обращаться. Зависит от того, где объект создан: в блоке, внутри другого объекта или в куче. разобраны во второй части, «C++ в плагине: классы, шаблоны и время жизни». Владение объектами (ownership)владение объектом (ownership) — Ответственность за удаление объекта. Владелец удаляет объект, наблюдатели только пользуются им через указатель или ссылку и должны перестать обращаться к нему до удаления. в куче (heap)куча (heap) — Динамическая память для объектов, созданных выражением new. Такой объект живёт до явного delete или до удаления его владельцем, например умным указателем. — в отдельной статье «Кто владеет объектом», а правила для аудиопотока (audio thread)аудиопоток (audio thread) — Поток с повышенным приоритетом, который вызывает обработку звука для каждого блока. Каждый вызов должен закончиться до срока блока, поэтому в нём не выделяют память, не берут блокировки и не обращаются к диску. — в «C++ в аудиопотоке».
Как код попадает в компилятор
Перед компиляцией каждый файл проходит через препроцессор (preprocessor). Он обрабатывает строки, которые начинаются с #, и работает с текстом: типов и областей видимости он не знает. Директива #include вставляет содержимое другого файла на место директивы, как копирование текста. Импорт модулей, как в Python или Go, появился только в C++20, и код плагина его не использует.
#include <juce_core/juce_core.h> // угловые скобки: системные пути и пути библиотек
#include "tremolo_plugin.h" // кавычки: сначала рядом с текущим файломФайлы стандартной библиотеки подключаются без расширения: <vector>, <memory>, <ranges>, <cmath>. Заголовок, который может попасть в одну единицу компиляции дважды, начинается с #pragma once. Строка гарантирует, что файл будет вставлен один раз. Раньше для этого писали «стражи включения» #ifndef, #define, #endif; #pragma once в стандарт не входит, но поддерживается всеми основными компиляторами.
Объявления («что существует») принято писать в заголовок .h, определения («как работает») — в файл реализации .cpp. В PluginProcessor.h объявлен метод getName(), а его тело находится в PluginProcessor.cpp. Каждый .cpp вместе со всем, что он подключил, компилируется отдельно и называется единицей трансляции (translation unit). Затем компоновщик (linker) собирает единицы в один бинарный файл.
Модуль JUCE и «unity build»
Заголовки из include/Tremolo/ не подключают JUCE, а файлы из source/ не подключают свои заголовки. Это работает благодаря формату модуля JUCE. Заголовок tremolo_plugin.h подключает JUCE, стандартную библиотеку и все заголовки проекта в нужном порядке:
// tremolo_plugin.h
#include <juce_dsp/juce_dsp.h>
#include <vector>
#include <memory>
// ...
#include "include/Tremolo/Parameters.h"
#include "include/Tremolo/Tremolo.h"
#include "include/Tremolo/PluginProcessor.h"А tremolo_plugin.cpp подключает этот заголовок и затем вставляет через #include все файлы реализации:
// tremolo_plugin.cpp
#include "tremolo_plugin.h"
#include <TremoloPluginAssets.h>
#include "source/LfoVisualizer.cpp"
#include "source/Parameters.cpp"
#include "source/PluginProcessor.cpp"В итоге весь плагин компилируется как одна единица трансляции. Такой приём называют unity build. Для него важен порядок #include в tremolo_plugin.h: класс должен быть объявлен выше места, где его используют. Например, Tremolo.h стоит раньше PluginProcessor.h, который хранит поле типа Tremolo. А новый заголовок или .cpp нужно добавить в tremolo_plugin.h или tremolo_plugin.cpp, иначе компилятор его не увидит.
Unity build применяют ради скорости полной сборки: тяжёлые заголовки вроде juce_dsp.h компилятор разбирает один раз, а не в каждом .cpp. Модулю JUCE приём нужен ещё и для подключения: модуль добавляется в проект одним .cpp-файлом, который внутри вставляет остальные, и системе сборки не нужен список его исходников. Правка любого файла перекомпилирует всю единицу трансляции, поэтому после мелкого изменения сборка идёт дольше, чем при раздельной компиляции. Имена с внутренней связью и макросы из одного файла видны в следующих (см. анонимное пространство имён). Файл может компилироваться только потому, что нужный ему заголовок подключён выше в общей единице, и вне её он перестанет собираться.
Обычная схема — раздельная компиляция: каждый .cpp сам подключает нужные заголовки, и правка одного файла пересобирает только его. Повторный разбор общих заголовков в такой схеме сокращают предкомпилированные заголовки (precompiled headers), в CMake их задаёт команда target_precompile_headers (CMake 3.16). Склеивать исходники CMake тоже умеет сам: свойство цели UNITY_BUILD (CMake 3.16) объединяет файлы в пакеты во время сборки, а исходный код остаётся пригодным для раздельной компиляции. Модули C++20 (import вместо #include) решают ту же задачу средствами языка: интерфейс модуля компилируется один раз, а макросы импортирующего файла на него не влияют.
Макросы из CMake
const juce::String PluginProcessor::getName() const {
return TREMOLO_PLUGIN_NAME;
}TREMOLO_PLUGIN_NAME нигде в C++ не определён. Его передаёт компилятору CMake: в CMakeLists.txt проекта стоит TREMOLO_PLUGIN_NAME="${TREMOLO_PLUGIN_NAME}" в target_compile_definitions, что равносильно флагу -DTREMOLO_PLUGIN_NAME="Tremolo". Препроцессор заменит имя строкой "Tremolo".
Комментарии
// однострочный комментарий
/* многострочный
комментарий */
/** Комментарий документации (Doxygen).
* @code ... @endcode — пример кода внутри документации
*/Стандарт языка знает только // и /* */. Форма /** */ обычный комментарий для компилятора, но генератор документации Doxygen и среды разработки считают её описанием следующего объявления. Команды @param, @return, @code относятся к разметке Doxygen. В этой форме написано описание класса BypassTransitionSmoother. Комментарий после закрывающей скобки, например } // namespace tremolo, подсказывает, какой блок закрыт; это соглашение стиля, и clang-format умеет его проверять.
Типы и литералы
Таблица 1 показывает встроенные типы, которые чаще всего встречаются в коде плагина. Отсчёты (sample)отсчёт (sample) — Значение сигнала в один момент дискретизации, одно число. В звукозаписи его также называют сэмплом. аудио хранятся во float: для звука хватает точности, а данных вдвое меньше, чем с double. Частота дискретизации (sample rate)частота дискретизации (sample rate) — Число отсчётов в каждом канале за одну секунду, то же число кадров аудиоданных в секунду. Измеряется в герцах: 44 100 Гц означает 44 100 отсчётов на канал в секунду. и время — в double. Подробнее о представлении отсчётов — в статье «Аудиоданные».
| Тип | Что это | Пример из кода |
|---|---|---|
int | целое со знаком, обычно 32 бита | int expectedMaxFramesPerBlock |
float | число с плавающей точкой, обычно 32 бита | juce::AudioBuffer<float> |
double | число с плавающей точкой, обычно 64 бита | double sampleRate |
bool | true или false | bool acceptsMidi() const |
void | значения нет | void releaseResources() |
std::size_t
size_t не встроенный тип и не ключевое слово. Это имя беззнакового целого типа, выбранного реализацией. Полное имя std::size_t объявлено в заголовке <cstddef>. Тип предназначен для размеров объектов и индексов контейнеров стандартной библиотеки. В плагине он нужен на границе с JUCE: библиотека возвращает количество отсчётов как int, а std::vector и std::span ждут size_t.
lfoSamples.resize(4u * static_cast<size_t>(expectedMaxFramesPerBlock));Преобразование само знак не проверяет: отрицательное число, превращённое в size_t, станет огромным положительным. Поэтому оно записано явно, о чём подробнее в разделе про static_cast.
Литералы
Тип числового литерала задаёт его запись: 10 — int, 1.0 — double, 1.0f и 0.4f — float, 4u — unsigned int, 1uz (C++23) — size_t; последний встречается в StridedQueue.h. Суффикс f важен: без него литерал 0.4 имеет тип double, и при передаче в float компилятор предупредит о сужении. Его пишут даже у нуля: 0.f — то же, что 0.0f.
Строка в кавычках, например "Tremolo", не объект-строка, как в Python, а массив char с нулевым байтом на конце. Объекты строк — это классы std::string и juce::String; из литерала они создаются неявно. Сырой литерал R"(...)" (C++11) отключает экранирование и сохраняет переносы строк, с префиксом u8 — в кодировке UTF-8. Так в тестах плагина записан ожидаемый JSON:
const juce::String expectedOutput =
u8R"({
"__version__": 1,
"pluginName": "Tremolo",
"modulationRateHz": 10.0,
"bypassed": true,
"modulationWaveform": "Triangle"
})";Нулевой указатель обозначает nullptr (C++11). Раньше писали NULL или 0, но это целое число, и оно путалось с перегрузками функций. У nullptr собственный тип, и приводится он только к указателям: setLookAndFeel(nullptr) в деструкторе редактора.
auto, const и constexpr
auto (C++11) просит компилятор вывести тип переменной из выражения справа. Это не динамическая типизация: тип остаётся статическим и фиксируется при компиляции. Особенность в том, что auto отбрасывает ссылку и const верхнего уровня. Чтобы получить ссылку, пишут auto&; без амперсанда будет копия.
for (auto& lfo : lfos) { // ссылка на каждый осциллятор в массиве
lfo.reset();
}Если в этом цикле написать auto lfo, reset() сработает на копии осциллятора (oscillator)осциллятор (oscillator) — Генератор повторяющегося колебания. Частота задаёт число повторений за секунду, а форма волны — изменение значения внутри одного периода., а исходный не изменится.
const запрещает менять объект после инициализации. В коде плагина почти всё неизменяемое помечено const:
const auto inputSample = buffer.getSample(channelIndex, frameIndex);
const auto outputSample = modulationValue * inputSample;С указателями const читают справа налево. const auto* const name = "text"; означает: name — константный указатель (второй const) на константные символы (первый). Первый const запрещает менять символы через указатель, второй — перенаправлять сам указатель.
constexpr (C++11) требует, чтобы значение было известно во время компиляции. Его можно использовать там, где нужна константа: размер массива, параметр шаблона. constexpr подразумевает const. Так записаны, например, static constexpr auto modulationDepth = 0.4f; в Tremolo и constexpr auto versionHint = 1; при создании параметров.
Операторы и приведение типов
Условный оператор условие ? значение_если_да : значение_если_нет — выражение, а не инструкция. Через него выбирают целевое значение и подпись кнопки:
const auto target = bypass ? 1.0f : 0.0f;
bypassButton.setButtonText(bypassButton.getToggleState() ? "Bypassed" : "Bypass");Логические операторы && («и»), || («или») и ! («не») вычисляются лениво: если результат ясен по левой части, правая не выполняется. В isBusesLayoutSupported оператор != сравнивает два объекта juce::AudioChannelSet; он работает потому, что класс сам определил этот оператор (см. перегрузку операторов).
static_cast
static_cast<T>(x) — ключевое слово C++: явное преобразование выражения x к типу T. Угловые скобки похожи на шаблон, но это встроенная операция языка, и подключать для неё ничего не нужно. Компилятор проверяет, что такой переход между типами разрешён: static_cast<int>(3.7) допустим и даёт 3, а превратить указатель в несвязанный тип указателя нельзя. Сохранение значения при этом не гарантируется: возможна потеря дробной части, сужение или превращение отрицательного числа в беззнаковое.
dryBuffer.setSize(static_cast<int>(spec.numChannels),
static_cast<int>(spec.maximumBlockSize));В коде плагина static_cast встречается часто, потому что строгие предупреждения (см. раздел о предупреждениях) запрещают молча смешивать знаковые и беззнаковые числа. JUCE хранит размеры как int, а стандартная библиотека ждёт size_t. Приведение в стиле C, (int)x, допускает более широкий набор преобразований и хуже показывает намерение, поэтому в современном коде его избегают. Другие виды приведений в плагине не используются: const_cast снимает const, reinterpret_cast переинтерпретирует биты, dynamic_cast проверяет тип в иерархии классов во время выполнения.
Пространства имён
Пространство имён (namespace) группирует имена, чтобы они не конфликтовали с чужими. Код плагина лежит в tremolo, JUCE — в juce, стандартная библиотека — в std. Оператор разрешения области :: отделяет имя от области, где оно объявлено. В отличие от пакетов Go, пространство имён не связано с файлами: его можно открывать в любом числе файлов, содержимое объединяется.
juce::AudioBuffer<float> // класс AudioBuffer из пространства juce
juce::dsp::AudioBlock<float> // вложенное пространство juce::dsp
std::views::iota(0, n) // функция из std::views
Tremolo::LfoWaveform::sine // значение перечисления внутри класса
PluginProcessor::getName() // метод класса в определении вне классаПространство имён можно вложить в другое: juce::dsp — часть juce. Запись namespace tremolo::assets { ... } (C++17) равна двум вложенным блокам. Поиск имени идёт изнутри наружу: в PluginEditor.cpp написано assets::Background_png, хотя сгенерированные данные лежат в tremolo::assets. Код редактора находится внутри namespace tremolo, поэтому assets находится относительно него.
Анонимное пространство имён
Блок namespace { ... } без имени прячет объявления внутри единицы трансляции. Так устроены вспомогательные функции в Parameters.cpp: addParameterToProcessor и три функции create…Parameter нужны только этому файлу. У таких имён внутренняя связь (internal linkage): другой .cpp может объявить собственную функцию с тем же именем, и компоновщик их не спутает.
В этом проекте нужна оговорка. Из-за unity build все файлы source/*.cpp составляют одну единицу трансляции, поэтому анонимные пространства из разных файлов объединяются. Две вспомогательные функции с одинаковым именем в разных файлах всё равно будут конфликтовать. В заголовке такое пространство создаёт отдельные копии в каждой единице, поэтому для общего интерфейса оно не подходит.
Функции
Тип результата пишется перед именем. В определении вне класса имя метода уточняют именем класса. Компилятор должен увидеть объявление функции до её вызова, поэтому и нужны заголовки:
// PluginProcessor.h — объявление (сигнатура)
bool acceptsMidi() const override;
// PluginProcessor.cpp — определение (тело)
bool PluginProcessor::acceptsMidi() const {
return false;
}В объявлении имена параметров можно опустить, важны только типы: void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&);. Если параметр в теле не нужен, имя можно опустить и в определении. Если тело функции ещё не написано, JUCE предлагает вызвать juce::ignoreUnused(index);: пустая функция «использует» аргумент, и компилятор не предупреждает о неиспользуемой переменной. Стандартная альтернатива (C++17) — атрибут [[maybe_unused]] int index.
Параметр может иметь значение по умолчанию: explicit BypassTransitionSmoother(double crossfadeLengthSecondsValue = 0.01). Если аргумент не передан, подставится 0.01. Выражение по умолчанию вычисляется заново при каждом вызове без аргумента. В Python иначе: значение по умолчанию создаётся один раз при определении функции, поэтому список из def f(items=[]) общий для всех вызовов и накапливает элементы между ними. Задают значение по умолчанию только в объявлении.
Перегрузка
В C++ несколько функций могут иметь одно имя и разные списки параметров; компилятор выбирает подходящую по типам аргументов. У каждой перегрузки своё тело. У juce::AudioProcessor два метода processBlock: для AudioBuffer<float>& и для AudioBuffer<double>&.
int twice(int value) { return value * 2; }
double twice(double value) { return value * 2.0; }
// twice(3) вызовет первое тело, twice(3.5) — второе.В TypeScript перегрузка сигнатур устроена иначе: несколько записей описывают допустимые вызовы, а реализация одна и разветвляется внутри. В C++ ветвление выполняет компилятор при выборе функции.
inline и цепочки вызовов
В тестах плагина функция getFileOutputPath определена в заголовке TestUtils.h со словом inline. Заголовок подключается в несколько .cpp, и без inline в каждой единице трансляции появилось бы своё определение: компоновщик выдал бы ошибку «multiple definition». inline разрешает одинаковые определения в разных единицах и велит оставить одно. Исторически слово означало совет «встроить тело в место вызова», но сейчас главный смысл именно такой. Методы, тело которых написано прямо в классе, как в Tremolo.h, неявно inline.
Та же функция показывает цепочку вызовов: каждый метод возвращает объект, у которого вызывается следующий.
return juce::File::getSpecialLocation(
juce::File::SpecialLocationType::currentExecutableFile)
.getParentDirectory()
.getChildFile(fileName)
.getFullPathName()
.toStdString();Так же построены BusesProperties().withInput(...).withOutput(...) в конструкторе процессора. Шаблон называют «строитель» (builder).
Значение, ссылка и указатель
Это главное отличие C++ от Python, Go и TypeScript: автор кода выбирает, как передаётся объект. В Python и TypeScript объект всегда передаётся по ссылке на него. В Go структура по умолчанию копируется целиком. В C++ копируется любой объект, даже большой класс, если не сказано иное.
Таблица 2 сводит способы передачи с примерами из кода плагина. Обычно маленький тип (int, double, bool) передают по значению, входной объект — как const T&, а объект, который функция меняет, — как T&.
| Способ | Что получает функция | Пример из кода |
|---|---|---|
| по значению | копию аргумента | void setBypass(bool bypass) noexcept |
T& | другое имя того же объекта; функция может менять объект через это имя | void process(juce::AudioBuffer<float>& buffer) noexcept |
const T& | другое имя того же объекта; менять объект через это имя функция не может | void prepare(const juce::dsp::ProcessSpec& spec) |
T* | адрес объекта, может быть пустым | juce::AudioProcessorEditor* createEditor() override |
Ссылка T&
Ссылка — другое имя существующего объекта, изменения через неё меняют оригинал. В отличие от указателя, ссылка обязана быть инициализирована при объявлении, не бывает пустой, не может быть перенаправлена на другой объект и используется как обычная переменная: buffer.getNumSamples(), а не buffer->getNumSamples(). Структура Parameters хранит параметры как ссылки: juce::AudioParameterFloat& rate;.
Указатель T*
Указатель хранит адрес объекта. Он может быть nullptr и может указывать на другой объект после переприсваивания. Доступ к члену через указатель записывают ->, разыменование — *p. Тип const void* — указатель на байты неизвестного типа, которые нельзя менять. Так JUCE передаёт плагину сохранённое состояние: void setStateInformation(const void* data, int sizeInBytes). Объекта «массив с длиной» здесь нет, поэтому размер идёт отдельным параметром.
Метод buffer.getReadPointer(0) возвращает const float* — адрес первого отсчёта канала. Такой указатель вместе с длиной можно превратить в std::span (см. шаблоны).
Кто отвечает за удаление объекта, на который указывает указатель, — отдельный вопрос. Он разобран в статье «Кто владеет объектом».
Цикл по диапазону
Цикл for (элемент : коллекция) (C++11) похож на for x in xs в Python и for _, x := range xs в Go. Он работает с любым объектом, у которого есть begin() и end(). Запись const auto x копирует каждый элемент, и для чисел это верно. Для больших объектов пишут const auto& x, чтобы не копировать, и auto& x, чтобы менять элементы на месте.
Вместо классического for (int i = 0; i < n; ++i) код плагина использует std::views::iota (C++20, заголовок <ranges>): ленивую последовательность целых чисел в полуоткрытом интервале [начало, конец), как range(a, b) в Python. Массив в памяти не создаётся, а индекс можно объявить const.
for (const auto frameIndex : std::views::iota(0, buffer.getNumSamples())) {
for (const auto channelIndex : std::views::iota(0, buffer.getNumChannels())) {
const auto inputSample = buffer.getSample(channelIndex, frameIndex);
buffer.setSample(channelIndex, frameIndex, modulationValue * inputSample);
}
}Оба аргумента iota должны быть одного типа. getNumSamples() возвращает int, поэтому начало записано как 0, а не 0u.
noexcept и [[nodiscard]]
noexcept (C++11) обещает, что функция не выбрасывает исключений. Если исключение всё же вылетит, программа завершится через std::terminate. Компилятор может сгенерировать более компактный код, а стандартные контейнеры учитывают noexcept при выборе между копированием и перемещением. В аудиокоде это ещё и документация: методы, которые вызываются из аудиопотока (process, setBypass, reset), не должны бросать исключения, как и выделять память или блокироваться.
void setBypass(bool bypass) noexcept { /* ... */ }
[[nodiscard]] bool isTransitioning() const noexcept { /* ... */ }Атрибут [[nodiscard]] (C++17) просит компилятор предупредить, если результат функции проигнорирован: вызывать isTransitioning(); и не использовать ответ бессмысленно. Двойные квадратные скобки — стандартный синтаксис атрибутов (C++11), отличный от [] индексирования и от захвата лямбды. Атрибут nodiscard — не ключевое слово вроде const, а имя атрибута. Другие атрибуты: [[maybe_unused]], [[fallthrough]], [[likely]]. Правила описаны в стандарте.
Точка входа плагина
У плагина нет функции main(). Вместо неё JUCE ищет функцию с фиксированным именем, которая создаёт процессор:
} // namespace tremolo
// This creates new instances of the plugin.
// This function definition must be in the global namespace.
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() {
return new tremolo::PluginProcessor();
}Функция определена вне namespace tremolo, в глобальном пространстве: внутри пространства её полное имя стало бы tremolo::createPluginFilter, и JUCE её бы не нашёл. Макрос JUCE_CALLTYPE задаёт соглашение о вызове, то есть порядок передачи аргументов. Оно нужно, чтобы плагин и хост одинаково понимали вызов; на Windows это __stdcall, на других платформах макрос пуст.
Выражение new T(...) создаёт объект в куче и возвращает указатель на него. Владение переходит обёртке плагина JUCE, и она позже вызовет delete. Возвращается указатель на базовый класс juce::AudioProcessor*, хотя создан tremolo::PluginProcessor: объект наследника можно использовать везде, где ждут базовый класс (см. наследование). В собственном коде голые new и delete обычно не пишут: для этого есть std::make_unique, подробнее — в «Кто владеет объектом».
Предупреждения компилятора
Файл cmake/CompilerWarnings.cmake включает строгий набор предупреждений, и он объясняет несколько особенностей стиля кода. Таблица 3 связывает флаги с тем, как они видны в коде.
| Флаг | Что проверяет | Как это видно в коде |
|---|---|---|
-Wunused | неиспользуемые переменные и параметры | juce::ignoreUnused(index) |
-Wconversion, -Wsign-conversion, -Wshorten-64-to-32 | неявные преобразования со знаком и сужением | static_cast<size_t>(...), static_cast<int>(...) |
-Woverloaded-virtual | виртуальная функция базы скрыта перегрузкой | using AudioProcessor::processBlock; |
-Wreorder | порядок списка инициализации не совпадает с порядком полей | порядок полей в BypassTransitionSmoother |
-Wshadow | локальное имя скрывает внешнее | параметр конструктора назван crossfadeLengthSecondsValue, а не crossfadeLengthSeconds |
-Wfloat-equal | сравнение вещественных чисел через == | в тестах EXPECT_NEAR и EXPECT_FLOAT_EQ |
-Wzero-as-null-pointer-constant | 0 вместо nullptr | setLookAndFeel(nullptr) |
-Wno-ignored-qualifiers | отключено: бессмысленный const у возвращаемого значения | const juce::String getName() const override |
Предупреждения проверяют не только стиль. Смешение знаковых и беззнаковых чисел, неправильный порядок инициализации и скрытая перегрузка виртуального метода приводят к ошибкам, которые трудно искать при выполнении.
Какой тип у литерала 0.4 без суффикса и почему в коде пишут 0.4f?
Литерал 0.4 имеет тип double. При передаче в параметр типа float Clang с флагом -Wconversion предупредит о сужении double → float. Суффикс f делает литерал float сразу, и преобразования нет.
Что случится, если в цикле for (auto lfo : lfos) вызвать lfo.reset()?
auto отбрасывает ссылку, поэтому lfo — копия элемента массива. reset() сработает на копии, а осцилляторы в lfos не изменятся. Чтобы менять элементы на месте, нужна запись for (auto& lfo : lfos).
Зачем в tremolo_plugin.h порядок директив #include важен, а в обычном проекте с отдельными .cpp-файлами это не так?
Плагин собирается как одна единица трансляции: все .cpp вставляются в tremolo_plugin.cpp после общего заголовка. Компилятор читает текст сверху вниз и должен увидеть объявление класса раньше его использования, поэтому Tremolo.h идёт раньше PluginProcessor.h. В проекте, где каждый .cpp подключает нужные заголовки сам, порядок между файлами задаёт не общий заголовок, а директивы каждого файла.
Почему createPluginFilter определена вне namespace tremolo?
JUCE находит функцию по фиксированному имени createPluginFilter в глобальном пространстве. Внутри namespace tremolo её полное имя было бы tremolo::createPluginFilter, и хост-обёртка JUCE её не нашла бы.
Источники
- Alex. Learn C++. Главы 2 «C++ Basics: Functions and Files» (уроки 2.7, 2.8, 2.10, 2.11, 2.12), 4, 5, 7, 10, 11, 12 и 16. в списке литературы → — препроцессор, заголовки, типы, литералы, пространства имён, приведение типов, перегрузка, ссылки, указатели и циклы.
- cppreference. static_cast — допустимые преобразования; std::size_t — определение и заголовок
<cstddef>; noexcept specifier и nodiscard — семантика и стандарт появления. - Проект стандарта C++ (eel.is/c++draft): support.types.layout —
size_t, namespace.unnamed — анонимное пространство имён, dcl.attr.grammar — синтаксис атрибутов. - JUCE. JUCE Module Format — модуль из одного
.cpp, который вставляет остальные исходники, и блокBEGIN_JUCE_MODULE_DECLARATION. - CMake. UNITY_BUILD и target_precompile_headers — автоматический unity build и предкомпилированные заголовки (CMake 3.16).
- cppreference. Modules — модули C++20 и отличие
importот#include. - Doxygen. Documenting the code — формы комментариев документации.
- JUCE. tremolo-juce-course — фрагменты кода из папки
complete:tremolo_plugin.h,tremolo_plugin.cpp,PluginProcessor.h,PluginProcessor.cpp,Parameters.cpp,Tremolo.h,BypassTransitionSmoother.h,cmake/CompilerWarnings.cmake,CMakeLists.txtиtest/source/TestUtils.h.