一键导入
modern-cpp
C++20 best practices for ESP32/embedded, aligned with Google C++ Style Guide. Use when writing or reviewing C++ code.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
C++20 best practices for ESP32/embedded, aligned with Google C++ Style Guide. Use when writing or reviewing C++ code.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Complete RF protocol reference for Elero wireless blinds - packet structure, encryption, commands, state constants. Use when working with RF packets, encryption/decryption, command bytes, CC1101 configuration, or protocol debugging.
ESP32 memory management, ISRs, FreeRTOS tasks, SPI, power management, ESPHome components. Use when writing ESP32 code, handling interrupts, managing memory, configuring SPI, or implementing ESPHome components.
Review checklist for embedded/component-based changes. Use before reviewing PRs, refactors, or new features to check lifecycle, interface design, separation of concerns, and debuggability.
SX1262 radio driver reference for Heltec WiFi LoRa 32 V4 — register config, GFSK TX/RX, errata fixes, CC1101 interop, FEM PA control. Use when debugging SX1262 TX/RX, modifying radio config, or comparing against reference implementations.
基于 SOC 职业分类
| name | modern-cpp |
| description | C++20 best practices for ESP32/embedded, aligned with Google C++ Style Guide. Use when writing or reviewing C++ code. |
Guidelines for safe, efficient embedded C++ code based on the Google C++ Style Guide, adapted for ESP32/embedded conventions.
Source: Google C++ Style Guide
C++ Version: C++20 is supported on ESP-IDF 5.x (GCC 12.2+) and Arduino-ESP32 2.x+ (with build flag override). See Tooling section for build configuration.
Toolchain: ESP-IDF 5.x ships GCC 12.2. C++20 core language features are fully supported. Most <concepts>, <span>, <ranges> headers work. std::format is NOT available (libstdc++ 12 doesn't ship it). Use snprintf instead.
| Entity | Style | Example |
|---|---|---|
| Types (classes, structs, enums) | PascalCase | UrlTable, SensorState |
| Variables | snake_case | table_name, num_entries |
| Class data members | trailing _ | table_name_, count_ |
| Struct data members | no trailing _ | x, y |
| Constants | UPPER_SNAKE with prefix | ELERO_MAX_RETRIES |
| Functions | PascalCase | ReadSensor(), ProcessData() |
| Accessors/mutators | snake_case | count(), set_count() |
| Namespaces | snake_case | esphome::elero |
| Macros | UPPER_SNAKE with prefix | ELERO_MAX_SIZE |
| Enumerators | PascalCase or UPPER_SNAKE | kIdle, STATE_IDLE |
Note on constants: Google style uses kPascalCase, but ESP-IDF, ESPHome, and Arduino all use UPPER_SNAKE_CASE for constants. Follow ecosystem conventions for embedded code.
class SensorReader {
public:
void UpdateReading();
int reading() const { return reading_; }
private:
static constexpr int MAX_RETRIES = 3;
int reading_{0};
};
enum class State : uint8_t { IDLE, RUNNING, ERROR };
Use fixed-width types from <cstdint> when size matters. Use int for general purposes.
uint8_t register_value; // Hardware register
int32_t packet_count; // Known size needed
for (int i = 0; i < n; ++i) // General loop counter
Avoid: unsigned just for non-negativity, short/long/long long.
ESP32/embedded code doesn't use exceptions. Use return values instead.
[[nodiscard]] bool WriteRegister(uint8_t addr, uint8_t val);
[[nodiscard]] std::optional<Data> Parse(std::string_view input);
enum class Status { OK, INVALID_ARG, TIMEOUT };
[[nodiscard]] Status Initialize();
Use [[nodiscard]] on functions where ignoring the return value is likely a bug.
// Good: Compile-time constants
constexpr int MAX_RETRIES = 3;
constexpr uint32_t TIMEOUT_MS = 5000;
inline constexpr uint8_t ENCODE_TABLE[] = {0x08, 0x02, 0x0d}; // Header-only
// C++20: Use consteval for guaranteed compile-time evaluation
consteval uint32_t hash(const char *s) { /* ... */ }
// Avoid
#define MAX_SIZE 100 // Use constexpr
const int MAX = ComputeMax(); // Runtime init - use constexpr if possible
static const uint8_t TABLE[] = {}; // Creates copy per TU - use inline
// Unique ownership
auto widget = std::make_unique<Widget>();
// RAII guard pattern
class SpiTransaction {
public:
explicit SpiTransaction(SpiDevice* dev) : dev_(dev) { dev_->Enable(); }
~SpiTransaction() { dev_->Disable(); }
SpiTransaction(const SpiTransaction&) = delete;
SpiTransaction& operator=(const SpiTransaction&) = delete;
private:
SpiDevice* dev_;
};
Use concepts to constrain templates. Provides clear error messages at the call site instead of deep template instantiation failures.
#include <concepts>
// Constrain callable parameters
template<std::invocable<Device &> F>
void for_each_active(F &&fn);
// Define project-specific concepts
template<typename T>
concept OutputAdapterLike = requires(T a, const Device &d) {
a.on_device_added(d);
a.on_state_changed(d);
a.setup(std::declval<DeviceRegistry &>());
};
// Constrain with requires clause
template<typename T>
requires std::is_trivially_copyable_v<T>
void persist(const T &data);
When to use concepts:
std::invocable)std::input_range, std::contiguous_range)requires)Avoid: Over-constraining simple templates. If a template only has 1-2 call sites, a concept adds ceremony without value.
Use std::span for non-owning views of contiguous data. Replaces (T* ptr, size_t len) pairs.
#include <span>
// Before (C++17): raw pointer + length
void process_buffer(const uint8_t *data, size_t len);
// After (C++20): self-documenting, bounds-aware
void process_buffer(std::span<const uint8_t> data);
// Fixed-size span (compile-time size check)
void process_packet(std::span<const uint8_t, 64> packet);
// Works with arrays, vectors, std::array — zero cost
uint8_t buf[64];
process_buffer(buf); // Implicit conversion
process_buffer({buf, 32}); // Subspan
process_buffer(std::span{buf}.first(n)); // Dynamic subspan
When to use std::span:
const uint8_t *data, uint8_t len pairsWhen NOT to use std::span:
write_byte())const T&)Designated initializers are now standard C++20 (previously a GCC extension).
struct Context {
uint32_t open_duration_ms{0};
uint32_t close_duration_ms{0};
uint32_t timeout_ms{120000};
};
// Clear, self-documenting initialization
auto ctx = Context{
.open_duration_ms = 10000,
.close_duration_ms = 8000,
.timeout_ms = 60000,
};
// Struct: passive data, no invariants
struct Point { float x; float y; };
// Class: has invariants, methods, or non-public members
class Circle {
public:
explicit Circle(float r) : radius_(r) {}
float Area() const;
private:
float radius_;
};
class MyClass {
public:
// Types, constants, constructors, methods, accessors
protected:
// Protected members
private:
// Private methods, then data members last
};
// Rule of Zero: Let compiler generate special members
class Widget {
std::string name_;
std::vector<int> data_;
};
// Rule of Five: If you define one, define all
class RawResource {
public:
~RawResource();
RawResource(const RawResource&);
RawResource& operator=(const RawResource&);
RawResource(RawResource&&) noexcept;
RawResource& operator=(RawResource&&) noexcept;
};
// Move-only
class UniqueHandle {
public:
UniqueHandle(const UniqueHandle&) = delete;
UniqueHandle& operator=(const UniqueHandle&) = delete;
UniqueHandle(UniqueHandle&&) noexcept = default;
UniqueHandle& operator=(UniqueHandle&&) noexcept = default;
};
override and final explicitlyclass Base {
public:
virtual void Process() = 0;
virtual ~Base() = default;
};
class Derived : public Base {
public:
void Process() override;
};
| Type | When |
|---|---|
T (value) | Small types (≤16 bytes), will copy anyway |
const T& | Large types, read-only |
T* | Nullable output parameter |
std::string_view | Read-only strings |
std::span<const T> | Read-only contiguous data (C++20) |
explicit on single-argument constructorsUse auto only when it improves clarity.
// Good: Type obvious
auto widget = std::make_unique<Widget>();
auto it = map.find(key);
auto [key, value] = *it;
// Bad: Type unclear
auto result = Compute(); // What type?
++i; // Prefer over i++ when value not used
memset(&data, 0, sizeof(data)); // Prefer sizeof(var) over sizeof(Type)
void Process(const std::string& input); // const parameters
int value() const { return value_; } // const methods
// Prefer: handle all enum values, no default
// This way the compiler warns if a new enumerator is added
switch (state) {
case State::IDLE:
break;
case State::RUNNING:
break;
case State::ERROR:
break;
}
// Unreachable return after switch satisfies -Wreturn-type
switch (state) {
case State::IDLE:
break;
case State::RUNNING:
[[fallthrough]]; // Explicit fallthrough
case State::ERROR:
break;
}
enum class State : uint8_t { IDLE, RUNNING }; // Not: enum State { ... }
static_cast<int>(x); // Not: (int)x
// Spaceship operator — generates all comparisons
auto operator<=>(const Point&) const = default;
#pragma once // Or traditional #ifndef guards
// Good: Single copy in flash
inline constexpr uint8_t ENCODE_TABLE[] = {0x08, 0x02};
// Bad: Copy per translation unit
static const uint8_t ENCODE_TABLE[] = {0x08, 0x02};
Use when only pointers/references needed to reduce compile dependencies.
if, for, while, switchThe compiler is your primary type checker. Use strict flags:
# Baseline (always use)
-Wall -Wextra -Wpedantic
# Recommended additions
-Wshadow # Variable shadowing
-Wconversion # Implicit type conversions
-Wsign-conversion # Signed/unsigned mismatches
-Wnull-dereference # Null pointer dereference
-Wold-style-cast # C-style casts
-Woverloaded-virtual # Hiding virtual functions
-Wformat=2 # Format string issues
-Wdouble-promotion # Float promoted to double
# For CI/strict builds
-Werror # Treat warnings as errors
ESP-IDF 5.x defaults to C++23 (-std=gnu++23). Arduino framework defaults to C++11. Override Arduino for C++20:
# ESPHome YAML (Arduino framework only — ESP-IDF already uses C++23)
esphome:
platformio_options:
build_unflags: -std=gnu++11
build_flags:
- -std=gnu++2a
# platformio.ini (Arduino)
build_unflags = -std=gnu++11
build_flags =
-std=gnu++2a
-Wall -Wextra -Wshadow -Wconversion
Note: Use -std=gnu++2a (not -std=c++20) for Arduino to keep GNU extensions that ESP32 Arduino core depends on. ESP-IDF configs don't need any override.
| Tool | Usage |
|---|---|
| clang-tidy | Gold standard linter. Use .clang-tidy config file |
| cppcheck | Fast, good for CI: cppcheck --enable=all src/ |
Example .clang-tidy:
Checks: >
-*,
bugprone-*,
cppcoreguidelines-*,
modernize-*,
performance-*,
readability-*,
-modernize-use-trailing-return-type,
-readability-magic-numbers
WarningsAsErrors: ''
HeaderFilterRegex: '.*'
Run: clang-tidy src/*.cpp -- -std=c++20 -I include/
Use clang-format with a .clang-format file:
# .clang-format (Google-based for ESP32)
BasedOnStyle: Google
IndentWidth: 2
ColumnLimit: 100
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: false
Run: clang-format -i src/*.cpp include/*.h
| Platform | std::atomic | Notes |
|---|---|---|
| ESP32 | Supported | Lock-free for 8/16/32-bit types, use freely |
| ESP8266 | Linker errors | undefined reference to '__atomic_exchange_1' |
| RP2040 | Linker errors | No hardware atomic support |
For ESP32-only projects: Use std::atomic — it's the correct solution for ISR-to-loop communication and avoids race conditions in read-modify-write patterns.
// ESP32-only: Preferred — no race condition
std::atomic<bool> received_{false};
void IRAM_ATTR isr() {
received_.store(true, std::memory_order_release);
}
void loop() {
if (received_.exchange(false)) { // Atomic read-and-clear
process();
}
}
For portable code (ESP8266/RP2040 support): Use volatile instead. Single bool read/write is inherently atomic on ARM and Xtensa, but read-modify-write patterns have race conditions.
// Portable but has race condition in read-clear pattern
volatile bool received_{false};
void IRAM_ATTR isr() {
received_ = true;
}
void loop() {
// Race: ISR can fire between read and clear
if (received_) {
received_ = false;
process();
}
}
When you need atomics (ESP32):
| Platform | RAM | Flash | Notes |
|---|---|---|---|
| ESP8266 | 80KB | 4MB | Very limited, avoid large buffers |
| ESP32 | 520KB | 4-16MB | Comfortable for most uses |
| RP2040 | 264KB | 2MB | Moderate constraints |
Prefer stack allocation for small objects, static allocation for fixed buffers.
| Feature | Header | Status | Notes |
|---|---|---|---|
| Concepts | <concepts> | Available | Use for template constraints |
std::span | <span> | Available | Non-owning buffer views |
| Designated initializers | (language) | Available | Standardized from GCC extension |
consteval | (language) | Available | Guaranteed compile-time |
constinit | (language) | Available | Guaranteed static init |
[[no_unique_address]] | (language) | Available | EBO for members |
| Three-way comparison | <compare> | Available | operator<=> |
std::ranges | <ranges> | Partial | Views work, some algorithms missing |
std::format | <format> | NOT available | Use snprintf instead |
| Modules | (language) | NOT available | GCC 12 has no module support |
| Coroutines | <coroutine> | Available but avoid | Too heavy for embedded |
| Avoid | Prefer |
|---|---|
new/delete | Smart pointers, containers |
(int)x | static_cast<int>(x) |
NULL | nullptr |
#define CONST 5 | constexpr int CONST = 5 |
typedef | using Alias = Type |
int arr[N] | std::array<int, N> |
| Output parameters | Return values |
i++ (unused value) | ++i |
static const in header | inline constexpr |
| Implicit conversions | explicit constructors |
volatile for ISR read-clear (ESP32) | std::atomic<bool> with .exchange() |
(T* buf, size_t len) pairs | std::span<T> (C++20) |
| Unconstrained templates | std::invocable, concepts (C++20) |
default: in exhaustive switch | Handle all cases, return after switch |