Avatar ·

Как условно включать атрибуты вроде `#[derive]` в Rust?

Всем привет! Я изучаю Rust и столкнулся с ситуацией, когда мне нужно, чтобы структура автоматически реализовывала трейт `Debug` только в режиме отладки (debug build), а в релизной сборке — нет. Я знаю про `#[cfg(debug_assertions)]`, который позволяет условно компилировать блоки кода. Но как применить это к атрибутам, особенно к `#[derive]`? Вот пример, который не работает: ```rust #[cfg(debug_assertions)] #[derive(Debug)] struct MyStruct { value: i32, } ``` Компилятор ругается, что атрибут `derive` не может быть условным. Я пробовал разные варианты: ставить `cfg` после `derive`, использовать вложенные атрибуты — ничего не помогает. Нашёл похожие обсуждения на англоязычных форумах, но они старые (2016–2018 годов). Может, с тех пор что-то изменилось? Или есть обходной путь? Конкретная задача: в дебаг-режиме хочу видеть `{:?}` для структуры, а в релизе — не хочу платить за лишний код. Подскажите, как правильно это сделать в современном Rust (edition 2021)? Заранее спасибо! <<>>

Почему нельзя просто поставить `cfg` на `derive`?

Атрибуты в Rust бывают двух типов: инертные (например, #[doc]) и процедурные (например, #[derive(Debug)], #[cfg]). #[derive] — это процедурный макрос, который выполняется на этапе компиляции и генерирует код. Проблема в том, что #[cfg] сам является процедурным атрибутом, и они не могут быть вложены друг в друга напрямую. Компилятор обрабатывает атрибуты последовательно, и #[cfg(debug_assertions)] не может «отключить» #[derive(Debug)], потому что сначала применяется derive, а потом cfg уже не имеет смысла.

Решение 1: Условная компиляция через макросы (рекомендуется)

Используйте cfg_attr — специальный атрибут, который применяет другой атрибут только при выполнении условия. Это стандартный способ для вашей задачи:

#[cfg_attr(debug_assertions, derive(Debug))]
struct MyStruct {
    value: i32,
}

Как это работает:

  • cfg_attr принимает два аргумента: условие и атрибут, который нужно применить.
  • Если условие истинно (например, debug_assertions включено), то derive(Debug) применяется к структуре.
  • Если условие ложно (релизная сборка), атрибут derive(Debug) игнорируется.

Полный пример:

#[cfg_attr(debug_assertions, derive(Debug))]
struct MyStruct {
    value: i32,
}

fn main() {
    let s = MyStruct { value: 42 };

    // В debug-сборке это скомпилируется, в release — нет
    #[cfg(debug_assertions)]
    println!("{:?}", s);
}

Решение 2: Условная реализация вручную

Если нужно больше контроля (например, реализовать несколько трейтов или добавить методы), можно использовать cfg на уровне impl-блока:

struct MyStruct {
    value: i32,
}

#[cfg(debug_assertions)]
impl std::fmt::Debug for MyStruct {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MyStruct")
            .field("value", &self.value)
            .finish()
    }
}

Это эквивалентно derive(Debug), но даёт гибкость для кастомной реализации. Однако для простых случаев cfg_attr лаконичнее.

Решение 3: Использование feature-флагов (для библиотек)

Если вы пишете библиотеку и хотите дать пользователям возможность включать Debug по требованию, используйте Cargo features:

// Cargo.toml
[features]
debug-impl = []

// lib.rs
#[cfg_attr(feature = "debug-impl", derive(Debug))]
pub struct MyStruct {
    value: i32,
}

Тогда пользователь может включить debug-impl в своём Cargo.toml: my_crate = { features = ["debug-impl"] }.

Важные замечания

  • cfg_attr работает с любыми атрибутами, не только с derive. Например: #[cfg_attr(windows, path = "win.rs")].
  • Условия могут быть сложными: #[cfg_attr(all(debug_assertions, feature = "extra"), ...)].
  • Не путайте cfg_attr с cfg: первый — это атрибут, который применяет другой атрибут; второй — условная компиляция целых блоков.

Начиная с Rust 1.0, cfg_attr стабилен и является идиоматическим решением для условных атрибутов. Ваш код на современном Rust (edition 2021) будет работать без проблем.

<<>>

Альтернативный подход: Использование трейта-заглушки

Если по каким-то причинам cfg_attr не подходит (например, вы работаете с устаревшей версией Rust или макросом, который не поддерживает cfg_attr), можно создать трейт-заглушку через условную компиляцию:

#[cfg(debug_assertions)]
use std::fmt::Debug;

#[cfg(not(debug_assertions))]
trait Debug {}

#[cfg_attr(debug_assertions, derive(Debug))]
struct MyStruct {
    value: i32,
}

// Теперь код с Debug будет компилироваться в обоих режимах,
// но в release режиме трейт Debug — пустой и не генерирует код.

Этот метод работает, но менее надёжен, так как требует ручного определения трейта и может конфликтовать с другими зависимостями. Используйте его только если cfg_attr по какой-то причине недоступен (например, при работе с очень старыми версиями Rust до 1.0).

Дополнительный совет: Измерение производительности

Если вы беспокоитесь о размере кода: derive(Debug) в релизной сборке не добавляет никаких накладных расходов, если вы не вызываете println!("{:?}", ...). Но если вы вызываете его условно (через #[cfg(debug_assertions)] на месте вызова), то компилятор выбросит мёртвый код. Так что ваша первоначальная мотивация верна — используйте cfg_attr на структуре и cfg на месте использования, чтобы избежать лишних зависимостей.