Основы C++ для JUCE: сборка, типы, функции и указатели

Что нужно знать о синтаксисе C++, чтобы читать код плагина на JUCE?

Опубликовано 15 мин чтения

В треках:Программист, этап 7

Код учебного плагина (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, и код плагина его не использует.

C++
#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, стандартную библиотеку и все заголовки проекта в нужном порядке:

C++
// 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 все файлы реализации:

C++
// 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

C++
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".

Комментарии

C++
// однострочный комментарий

/* многострочный
   комментарий */

/** Комментарий документации (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. Подробнее о представлении отсчётов — в статье «Аудиоданные».

Таблица 1. Встроенные типы в коде плагина. Размеры int, float и double указаны для типичных платформ: стандарт задаёт только минимальные диапазоны.
ТипЧто этоПример из кода
intцелое со знаком, обычно 32 битаint expectedMaxFramesPerBlock
floatчисло с плавающей точкой, обычно 32 битаjuce::AudioBuffer<float>
doubleчисло с плавающей точкой, обычно 64 битаdouble sampleRate
booltrue или falsebool acceptsMidi() const
voidзначения нетvoid releaseResources()

std::size_t

size_t не встроенный тип и не ключевое слово. Это имя беззнакового целого типа, выбранного реализацией. Полное имя std::size_t объявлено в заголовке <cstddef>. Тип предназначен для размеров объектов и индексов контейнеров стандартной библиотеки. В плагине он нужен на границе с JUCE: библиотека возвращает количество отсчётов как int, а std::vector и std::span ждут size_t.

C++
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:

C++
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&; без амперсанда будет копия.

C++
for (auto& lfo : lfos) {   // ссылка на каждый осциллятор в массиве
  lfo.reset();
}

Если в этом цикле написать auto lfo, reset() сработает на копии осциллятора (oscillator)осциллятор (oscillator) — Генератор повторяющегося колебания. Частота задаёт число повторений за секунду, а форма волны — изменение значения внутри одного периода., а исходный не изменится.

const запрещает менять объект после инициализации. В коде плагина почти всё неизменяемое помечено const:

C++
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; при создании параметров.

Операторы и приведение типов

Условный оператор условие ? значение_если_да : значение_если_нет — выражение, а не инструкция. Через него выбирают целевое значение и подпись кнопки:

C++
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, а превратить указатель в несвязанный тип указателя нельзя. Сохранение значения при этом не гарантируется: возможна потеря дробной части, сужение или превращение отрицательного числа в беззнаковое.

C++
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, пространство имён не связано с файлами: его можно открывать в любом числе файлов, содержимое объединяется.

C++
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 составляют одну единицу трансляции, поэтому анонимные пространства из разных файлов объединяются. Две вспомогательные функции с одинаковым именем в разных файлах всё равно будут конфликтовать. В заголовке такое пространство создаёт отдельные копии в каждой единице, поэтому для общего интерфейса оно не подходит.

Функции

Тип результата пишется перед именем. В определении вне класса имя метода уточняют именем класса. Компилятор должен увидеть объявление функции до её вызова, поэтому и нужны заголовки:

C++
// 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>&.

C++
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.

Та же функция показывает цепочку вызовов: каждый метод возвращает объект, у которого вызывается следующий.

C++
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&.

Таблица 2. Способы передачи параметров в коде плагина. Копия большого объекта, например аудиобуфера, расходует время и память, поэтому буфер передают по ссылке.
СпособЧто получает функцияПример из кода
по значениюкопию аргумента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.

C++
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), не должны бросать исключения, как и выделять память или блокироваться.

C++
void setBypass(bool bypass) noexcept { /* ... */ }
[[nodiscard]] bool isTransitioning() const noexcept { /* ... */ }

Атрибут [[nodiscard]] (C++17) просит компилятор предупредить, если результат функции проигнорирован: вызывать isTransitioning(); и не использовать ответ бессмысленно. Двойные квадратные скобки — стандартный синтаксис атрибутов (C++11), отличный от [] индексирования и от захвата лямбды. Атрибут nodiscard — не ключевое слово вроде const, а имя атрибута. Другие атрибуты: [[maybe_unused]], [[fallthrough]], [[likely]]. Правила описаны в стандарте.

Точка входа плагина

У плагина нет функции main(). Вместо неё JUCE ищет функцию с фиксированным именем, которая создаёт процессор:

C++
}  // 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 связывает флаги с тем, как они видны в коде.

Таблица 3. Флаги предупреждений GCC и Clang из CompilerWarnings.cmake и приёмы кода, которые они вызвали.
ФлагЧто проверяетКак это видно в коде
-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-constant0 вместо nullptrsetLookAndFeel(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 её не нашла бы.

Источники

Связи

Поиск

Ищем по названиям, тексту статей и английским терминам. Например: «частота дискретизации», «Nyquist», «аналоговый сигнал».