Як створити “пробіл” у структурі пам'яті класу C ++?


94

Питання

У контексті низькорівневого вбудованого металу , я хотів би створити порожній простір в пам'яті, в структурі C ++ і без будь-якого імені, щоб заборонити користувачеві доступ до такого розташування пам'яті.

Зараз я домігся цього, поставивши потворне uint32_t :96;бітове поле, яке зручно займе місце трьох слів, але воно викличе попередження від GCC (бітфілд занадто великий, щоб вмістити його в uint32_t), що цілком правомірно.

Хоча він працює нормально, він не дуже чистий, коли ви хочете поширити бібліотеку з кількома сотнями цих попереджень ...

Як це зробити правильно?

Чому виникає проблема в першу чергу?

Проект, над яким я працюю, полягає у визначенні структури пам'яті різних периферійних пристроїв цілої лінії мікроконтролерів (STMicroelectronics STM32). Для цього результатом є клас, який містить об’єднання декількох структур, які визначають усі регістри, залежно від цільового мікроконтролера.

Одним простим прикладом досить простого периферійного пристрою є наступний: вхід / вихід загального призначення (GPIO)

union
{

    struct
    {
        GPIO_MAP0_MODER;
        GPIO_MAP0_OTYPER;
        GPIO_MAP0_OSPEEDR;
        GPIO_MAP0_PUPDR;
        GPIO_MAP0_IDR;
        GPIO_MAP0_ODR;
        GPIO_MAP0_BSRR;
        GPIO_MAP0_LCKR;
        GPIO_MAP0_AFR;
        GPIO_MAP0_BRR;
        GPIO_MAP0_ASCR;
    };
    struct
    {
        GPIO_MAP1_CRL;
        GPIO_MAP1_CRH;
        GPIO_MAP1_IDR;
        GPIO_MAP1_ODR;
        GPIO_MAP1_BSRR;
        GPIO_MAP1_BRR;
        GPIO_MAP1_LCKR;
        uint32_t :32;
        GPIO_MAP1_AFRL;
        GPIO_MAP1_AFRH;
        uint32_t :64;
    };
    struct
    {
        uint32_t :192;
        GPIO_MAP2_BSRRL;
        GPIO_MAP2_BSRRH;
        uint32_t :160;
    };
};

Де все GPIO_MAPx_YYY- це макрос, який визначається як uint32_t :32або як тип реєстру (виділена структура).

Тут ви бачите, uint32_t :192;що працює добре, але викликає попередження.

Що я розглядав до цього часу:

Я міг би замінити його декількома uint32_t :32;(тут 6), але у мене є деякі крайні випадки, коли я маю uint32_t :1344;(42) (серед інших). Тому я волів би не додавати близько ста рядків поверх 8k інших, навіть незважаючи на те, що генерація структури виконана за сценарієм.

Точне попереджувальне повідомлення виглядає приблизно так: width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type(Мені просто подобається, як це тіньово).

Я волів би не вирішити це шляхом простого видалення попередження, а використання

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-WTheRightFlag"
/* My code */
#pragma GCC diagnostic pop

може бути рішенням ... якщо знайду TheRightFlag. Однак, як зазначено в цій темі , gcc/cp/class.cз цією сумною частиною коду:

warning_at (DECL_SOURCE_LOCATION (field), 0,
        "width of %qD exceeds its type", field);

Що говорить нам, що не існує -Wxxxпрапора для видалення цього попередження ...


26
ви розглядали char unused[12];і так далі?
М.М.

3
Я б просто придушив попередження. [class.bit] / 1 гарантує поведінку uint32_t :192;.
NathanOliver

3
@NathanOliver Я б також із задоволенням, але, схоже, це попередження не можна заблокувати (за допомогою GCC), або я не знайшов, як це зробити. Більше того, це все ще не є чистим способом (але це було б досить ситно). Мені вдалося знайти правильний прапор "-W", але не вдалося застосувати його лише до власних файлів (я не хочу, щоб користувач видаляв подібні застереження за свою роботу)
J Faucher,

3
До речі, :42*32замість цього ви можете писати:1344
MM

1
Спробувати це придушити попередження? gcc.gnu.org/onlinedocs/gcc/…
Хітобат

Відповіді:


36

Використовуйте кілька сусідніх анонімних бітових полів. Отже, замість:

    uint32_t :160;

наприклад, у вас буде:

    uint32_t :32;
    uint32_t :32;
    uint32_t :32;
    uint32_t :32;
    uint32_t :32;

По одному для кожного реєстру, для якого ви хочете бути анонімним.

Якщо у вас є великі простори для заповнення, це може бути зрозуміліше і менше помилок, схильних до використання макросів для повторення одного 32-бітного простору. Наприклад, враховуючи:

#define REPEAT_2(a) a a
#define REPEAT_4(a) REPEAT_2(a) REPEAT_2(a)
#define REPEAT_8(a) REPEAT_4(a) REPEAT_4(a)
#define REPEAT_16(a) REPEAT_8(a) REPEAT_8(a)
#define REPEAT_32(a) REPEAT_16(a) REPEAT_16(a)

Тоді простір 1344 (42 * 32 біт) можна додати таким чином:

struct
{
    ...
    REPEAT_32(uint32_t :32;) 
    REPEAT_8(uint32_t :32;) 
    REPEAT_2(uint32_t :32;)
    ...
};

Дякую за відповідь. Я вже вважав це, однак це додало б понад 200 рядків до деяких моїх файлів ( uint32_t :1344;є на місці), тому я волів би не їхати цим шляхом ...
J Faucher

1
@JFaucher Додав можливе рішення для вашої вимоги щодо підрахунку рядків. Якщо у вас є такі вимоги, ви можете згадати їх у питанні, щоб уникнути отримання відповідей, які не відповідають їм.
Кліффорд

Дякуємо за редагування та вибачаємось, що не вказали питання про кількість ліній. Моя думка полягає в тому, що мій код вже болісно занурюватися, оскільки там багато рядків, і я волів би уникати додавати занадто багато іншого. Тому я запитував, чи знає хтось «чистий» чи «офіційний» спосіб уникнути використання сусіднього анонімного бітового поля (навіть якщо це працює нормально). Макропідхід мені здається прекрасним. До речі, у вашому прикладі ви не отримуєте 36 * 32 біт?
J Faucher

@JFaucher - виправлено. Файли зіставлення реєстрів вводу-виводу обов'язково великі через велику кількість регістрів - зазвичай ви пишете один раз, і обслуговування не є проблемою, оскільки обладнання є константою. За винятком "приховування" реєстрів, ви робите для себе технічне обслуговування, якщо згодом вам потрібно отримати до них доступ. Ви, звичайно, знаєте, що всі пристрої STM32 вже мають заголовок карти реєстру, наданий постачальником? Це було б набагато менш схильним до помилок.
Кліффорд

2
Я погоджуюсь з вами і, справедливості заради, думаю, що я піду одним із тих двох методів, що відображаються у вашій відповіді. Я просто хотів бути впевненим, що C ++ не пропонує кращого рішення, перш ніж це зробити. Я добре знаю, що ST забезпечує ці заголовки, однак вони побудовані на масовому використанні макросів та побітових операцій. Мій проект полягає у створенні C ++, еквівалентного тим заголовкам, які будуть менш схильні до помилок (з використанням класів переліку, бітових полів тощо). Ось чому ми використовуємо скрипт для "перекладу" заголовків CMSIS в наші структури C ++ (і виявили деякі помилки у файлах ST до речі)
J Faucher

45

Як щодо C ++ - ішного способу?

namespace GPIO {

static volatile uint32_t &MAP0_MODER = *reinterpret_cast<uint32_t*>(0x4000);
static volatile uint32_t &MAP0_OTYPER = *reinterpret_cast<uint32_t*>(0x4004);

}

int main() {
    GPIO::MAP0_MODER = 42;
}

Ви отримуєте автозаповнення через GPIOпростір імен, і немає потреби в фіктивних відступів. Навіть, зрозуміліше, що відбувається, оскільки ви можете бачити адресу кожного реєстру, вам зовсім не доведеться покладатися на поведінку компілятора в заполнении.


1
Це, можливо, може оптимізувати менш добре, ніж структура для доступу до декількох регістрів MMIO від однієї і тієї ж функції. За допомогою вказівника на базову адресу в реєстрі компілятор може використовувати інструкції завантаження / зберігання з негайними переміщеннями, наприклад ldr r0, [r4, #16], тоді як компілятори частіше пропускають цю оптимізацію з кожною адресою, оголошеною окремо. GCC, ймовірно, завантажить кожну адресу GPIO в окремий реєстр. (З буквального пулу, хоча деякі з них можна представити як обернені безпосередні в кодуванні великого пальця.)
Пітер Кордес,

4
Виявляється, мої турботи були необгрунтованими; ARM GCC також оптимізує цей шлях. godbolt.org/z/ztB7hi . Але зверніть увагу, що ви хочете static volatile uint32_t &MAP0_MODER, ні inline. inlineЗмінна не компілюється. ( staticуникає наявності будь-якого статичного сховища для вказівника, і volatileсаме те, що ви хочете для MMIO, щоб уникнути усунення “мертвої пам’яті” або оптимізації запису / зворотного зчитування.)
Пітер Кордес,

1
@PeterCordes: вбудовані змінні - це нова функція C ++ 17. Але ти маєш рацію, staticоднаково добре справляється з цією справою. Дякуємо за згадку volatile, я додаю його до своєї відповіді (і змінити вбудований на статичний, тому він працює для попередньої версії C ++ 17).
geza

2
Це не є чітко чітко визначеною поведінкою. Дивіться цю тему твіттера, і, можливо, ця корисна
Шафік Ягмор

1
@JFaucher: створіть стільки просторів імен, скільки у вас є структур, і використовуйте автономні функції в цьому просторі імен. Отже, у вас буде GPIOA::togglePin().
geza

20

На арені вбудованих систем ви можете моделювати апаратне забезпечення, використовуючи структуру, або визначаючи вказівники на адреси реєстру.

Моделювання за структурою не рекомендується, оскільки компілятору дозволено додавати відступи між членами для цілей вирівнювання (хоча багато компілятори для вбудованих систем мають прагму для упаковки структури).

Приклад:

uint16_t * const UART1 = (uint16_t *)(0x40000);
const unsigned int UART_STATUS_OFFSET = 1U;
const unsigned int UART_TRANSMIT_REGISTER = 2U;
uint16_t * const UART1_STATUS_REGISTER = (UART1 + UART_STATUS_OFFSET);
uint16_t * const UART1_TRANSMIT_REGISTER = (UART1 + UART_TRANSMIT_REGISTER);

Ви також можете використовувати позначення масиву:

uint16_t status = UART1[UART_STATUS_OFFSET];  

Якщо вам потрібно використовувати структуру, IMHO, найкращим методом пропуску адрес буде визначення члена, а не доступ до нього:

struct UART1
{
  uint16_t status;
  uint16_t reserved1; // Transmit register
  uint16_t receive_register;
};

В одному з наших проектів ми маємо як константи, так і структури від різних постачальників (постачальник 1 використовує константи, тоді як постачальник 2 використовує структури).


Дякую за вашу відповідь. Однак я вирішив використовувати структурний підхід, щоб полегшити роботу користувача, коли він отримав функцію автозаповнення (у вас відображатимуться лише правильні атрибути), і я не хочу "показувати" користувачеві зарезервовані слоти як зазначив у коментарі до мого першого допису.
J Faucher

Ви все ще можете це зробити, зробивши вказані вище адреси staticчленами структури, припускаючи, що автозаповнення може показувати статичні члени. Якщо ні, це можуть бути також вбудовані функції-члени.
Phil1970,

@JFaucher Я не користувач вбудованих систем і не тестував цього, але чи не буде вирішено проблему автозаповнення, оголосивши зарезервованого члена приватним? (Ви можете оголосити приватних членів у структурі, і ви можете використовувати public:і private:скільки завгодно разів, щоб отримати правильне впорядкування полів.)
Натаніель

1
@ Натаніель: Не так; якщо клас має як publicі privateНЕ-статичні елементи даних, то це не стандартний тип макета , тому він не дає гарантії замовлення ви думаєте. (І я майже впевнений, що варіант використання OP вимагає стандартного типу макета.)
ruakh

1
Не забувайте volatileпро ці декларації, до речі, для відображених у пам’яті регістрів вводу-виводу.
Пітер Кордес,

13

geza має рацію, що ви дійсно не хочете використовувати для цього класи.

Але якщо ви хотіли наполягати, найкращий спосіб додати невикористаний член шириною n байт - це просто зробити це:

char unused[n];

Якщо ви додасте конкретну реалізацію прагми, щоб запобігти додаванню довільних відступів для членів класу, це може спрацювати.


Для GNU C / C ++ (gcc, clang та інші, що підтримують однакові розширення), одним із допустимих місць для розміщення атрибуту є:

#include <stddef.h>
#include <stdint.h>
#include <assert.h>  // for C11 static_assert, so this is valid C as well as C++

struct __attribute__((packed)) GPIO {
    volatile uint32_t a;
    char unused[3];
    volatile uint32_t b;
};

static_assert(offsetof(struct GPIO, b) == 7, "wrong GPIO struct layout");

(приклад у провіднику компілятора Godbolt показує offsetof(GPIO, b)= 7 байт.)


9

Щоб розширити відповіді @ Clifford's та @Adam Kotwasinski:

#define REP10(a)        a a a a a a a a a a
#define REP1034(a)      REP10(REP10(REP10(a))) REP10(a a a) a a a a

struct foo {
        int before;
        REP1034(unsigned int :32;)
        int after;
};
int main(void){
        struct foo bar;
        return 0;
}

Я включив варіант своєї пропозиції у свою відповідь, дотримуючись подальших вимог у коментарі. Кредит, де кредит виплачується.
Кліффорд

7

Щоб розширити відповідь Кліффорда, ви завжди можете зробити макрос анонімними бітовими полями.

Так замість

uint32_t :160;

використання

#define EMPTY_32_1 \
 uint32_t :32
#define EMPTY_32_2 \
 uint32_t :32;     \ // I guess this also can be replaced with uint64_t :64
 uint32_t :32
#define EMPTY_32_3 \
 uint32_t :32;     \
 uint32_t :32;     \
 uint32_t :32
#define EMPTY_UINT32(N) EMPTY_32_ ## N

А потім використовуйте це як

struct A {
  EMPTY_UINT32(3);
  /* which resolves to EMPTY_32_3, which then resolves to real declarations */
}

На жаль, вам знадобиться стільки EMPTY_32_Xваріантів, скільки байт у вас є :( Тим не менше, це дозволяє вам мати окремі оголошення у вашій структурі.


5
Використовуючи макроси Boost CPP, я думаю, ви можете використовувати рекурсію, щоб уникнути необхідності створювати вручну всі необхідні макроси.
Пітер Кордес,

3
Ви можете їх каскадувати (до межі рекурсії препроцесора, але це зазвичай достатньо). Так #define EMPTY_32_2 EMPTY_32_1; EMPTY_32_1і #define EMPTY_32_3 EMPTY_32_2; EMPTY_32_1т. Д.
Мірал

Можливо, @PeterCordes, але теги вказують на те, що, можливо, потрібна сумісність кабіни C та C ++.
Кліффорд

2
C і C ++ використовують один і той же препроцесор C; Я не бачу іншої проблеми, крім можливо надання необхідного заголовка boost для C. Вони дійсно поміщають матеріали CPP-макросу в окремий заголовок.
Пітер Кордес,

1

Визначити великий пробіл як групи по 32 біти.

#define M_32(x)   M_2(M_16(x))
#define M_16(x)   M_2(M_8(x))
#define M_8(x)    M_2(M_4(x))
#define M_4(x)    M_2(M_2(x))
#define M_2(x)    x x

#define SPACER int : 32;

struct {
    M_32(SPACER) M_8(SPACER) M_4(SPACER)
};

1

Я думаю, було б вигідно ввести ще якусь структуру; що, в свою чергу, може вирішити проблему розпірок.

Назвіть варіанти

Незважаючи на те, що плоскі простори імен приємні, проблема полягає в тому, що у вас виходить строката колекція полів і не простий спосіб передачі всіх пов’язаних полів разом. Крім того, використовуючи анонімні структури в анонімному об'єднанні, ви не можете передавати посилання на самі структури або використовувати їх як параметри шаблону.

Отже, на першому кроці я міг би розглянути наступнеstruct :

// GpioMap0.h
#pragma once

// #includes

namespace Gpio {
struct Map0 {
    GPIO_MAP0_MODER;
    GPIO_MAP0_OTYPER;
    GPIO_MAP0_OSPEEDR;
    GPIO_MAP0_PUPDR;
    GPIO_MAP0_IDR;
    GPIO_MAP0_ODR;
    GPIO_MAP0_BSRR;
    GPIO_MAP0_LCKR;
    GPIO_MAP0_AFR;
    GPIO_MAP0_BRR;
    GPIO_MAP0_ASCR;
};
} // namespace Gpio

// GpioMap1.h
#pragma once

// #includes

namespace Gpio {
struct Map1 {
    // fields
};
} // namespace Gpio

// ... others headers ...

І нарешті, загальний заголовок:

// Gpio.h
#pragma once

#include "GpioMap0.h"
#include "GpioMap1.h"
// ... other headers ...

namespace Gpio {
union Gpio {
    Map0 map0;
    Map1 map1;
    // ... others ...
};
} // namespace Gpio

Тепер я можу написати void special_map0(Gpio:: Map0 volatile& map);, а також швидко переглянути короткий огляд усіх доступних архітектур.

Прості розпірки

З визначенням, розділеним на кілька заголовків, заголовки стають набагато більш керованими.

Тому моїм початковим підходом точно відповідати вашим вимогам було б дотримуватися повторення std::uint32_t:32;. Так, він додає кілька рядків 100-х до існуючих 8-лінійних рядків, але оскільки кожен заголовок індивідуально менший, це може бути не так погано.

Якщо ви готові розглянути більш екзотичні рішення, проте ...

Представляємо $.

Маловідомий факт $є життєздатним символом ідентифікаторів С ++; це навіть життєздатний початковий символ (на відміну від цифр).

Поява $у вихідному коді, швидше за все, підніме брови, і $$$$, безумовно, приверне увагу під час огляду коду. Цим можна легко скористатися:

#define GPIO_RESERVED(Index_, N_) std::uint32_t $$$$##Index_[N_];

struct Map3 {
    GPIO_RESERVED(0, 6);
    GPIO_MAP2_BSRRL;
    GPIO_MAP2_BSRRH;
    GPIO_RESERVED(1, 5);
};

Ви навіть можете скласти простий "вовк" як гачок перед фіксацією або у вашому інтерфейсі, який шукає $$$$в коді C ++, що фіксується, і відхилити такі коміти.


1
Пам'ятайте, що конкретний випадок використання OP - це опис відображених на пам'ять регістрів вводу-виводу до компілятора. Він ніколи не має сенсу скопіювати всю - структуру за значенням. (І GPIO_MAP0_MODER, мабуть, кожен член подобається volatile.) Можливо, корисне використання посилань або параметрів шаблону раніше анонімних членів. І для загального випадку конструкцій прокладки, звичайно. Але приклад використання пояснює, чому ОП залишив їх анонімними.
Пітер Кордес,

Ви можете використати, $$$padding##Index_[N_];щоб зробити назву поля більш зрозумілою на випадок, якщо воно коли-небудь з’явилося при автозаповненні або під час налагодження. (Або zz$$$paddingзробити так, щоб це сортувалося за GPIO...іменами, тому що вся суть цієї вправи згідно з OP є більш приємним автозаповненням для імен місцезнаходжень вводу-виводу, що відображаються на пам’ять.)
Пітер Кордес,

@PeterCordes: Я перевірив відповідь ще раз, щоб перевірити, і ніколи не бачив жодної згадки про копіювання. volatileОднак я забув кваліфікатор на посиланні, який був виправлений. Що стосується іменування; Дозволю до ОП. Існує багато варіацій (заповнення, зарезервоване, ...), і навіть "найкращий" префікс для автозавершення може залежати від IDE, що знаходиться під рукою, хоча я ціную ідею налаштування сортування.
Matthieu M.

Я мав на увазі " і не простий спосіб передачі всіх пов’язаних полів разом ", що звучить як присвоєння структури, а решту речення про іменування членів структури об’єднання.
Пітер Кордес,

1
@PeterCordes: Я думав передати посилання, як показано далі. Мені незручно, що структура OP заважає їм створювати "модулі", для яких можна статично довести, що вони мають доступ лише до певної архітектури (беручи посилання на конкретну struct), і що unionкінцеві результати розповсюджуються скрізь, навіть у специфічних для архітектури бітах, які могли б менше піклуватися про інших.
Matthieu M.

0

Хоча я згоден, що структури не повинні використовуватися для доступу до порту вводу-виводу MCU, на оригінальне запитання можна відповісти таким чином:

struct __attribute__((packed)) test {
       char member1;
       char member2;
       volatile struct __attribute__((packed))
       {
       private:
              volatile char spacer_bytes[7];
       }  spacer;
       char member3;
       char member4;
};

Можливо, вам доведеться замінити __attribute__((packed))на #pragma packабо подібне залежно від синтаксису компілятора.

Змішування приватних та загальнодоступних членів у структурі зазвичай призводить до того, що розміщення пам'яті більше не гарантується стандартом C ++. Однак якщо всі нестатичні члени структури є приватними, це все одно вважається POD / стандартним макетом, а також структури, які їх вбудовують.

З якоїсь причини gcc видає попередження, якщо член анонімної структури є приватним, тому мені довелося дати йому ім'я. Крім того, загортання його в чергову анонімну структуру також позбавляє попередження (це може бути помилка).

Зверніть увагу, що spacerчлен сам не є приватним, тому доступ до даних все одно можна отримати таким чином:

(char*)(void*)&testobj.spacer;

Однак такий вираз виглядає як очевидний хак, і, сподіваємось, не буде використаний без дійсно вагомих причин, не кажучи вже про помилку.


1
Користувачі не можуть оголошувати ідентифікатори в будь-якому просторі імен, що містить подвійні підкреслення в будь-якому місці імені (в C ++ або лише на початку в C); це робить код неправильно сформованим. Ці імена зарезервовані для реалізації, і тому теоретично можуть конфліктувати з вашими жахливо тонкими та примхливими способами. У будь-якому випадку, компілятор не зобов'язаний зберігати ваш код, якщо він їх містить. Такі імена не є швидким способом отримати "внутрішні" імена для власного використання.
underscore_d

Дякую, виправили.
Jack White

-1

Анти-розчин.

НЕ РОБІТЬ ЦЕ: Поєднуйте приватне та публічне поля.

Можливо, макрос з лічильником для створення імен змінних uniqie буде корисним?

#define CONCAT_IMPL( x, y ) x##y
#define MACRO_CONCAT( x, y ) CONCAT_IMPL( x, y )
#define RESERVED MACRO_CONCAT(Reserved_var, __COUNTER__) 


struct {
    GPIO_MAP1_CRL;
    GPIO_MAP1_CRH;
    GPIO_MAP1_IDR;
    GPIO_MAP1_ODR;
    GPIO_MAP1_BSRR;
    GPIO_MAP1_BRR;
    GPIO_MAP1_LCKR;
private:
    char RESERVED[4];
public:
    GPIO_MAP1_AFRL;
    GPIO_MAP1_AFRH;
private:
    char RESERVED[8];
};


3
Гаразд. Якщо ніхто не заперечує, я залишу відповідь як не робити.
Роберт Анджеюк,

4
@NicHartley Враховуючи кількість відповідей, ми близькі до "дослідницького" питання. У дослідженнях знання про глухий кут - це все-таки знання, воно уникає інших піти неправильним шляхом. +1 за хоробрість.
Олів,

1
@Oliv І я -1, оскільки ОП вимагав чогось, ця відповідь порушила вимогу, і тому це погана відповідь. Я прямо не зробив жодного ціннісного судження, позитивного чи негативного, щодо людини, ні в одному з коментарів - лише щодо відповіді. Я думаю, ми обидва можемо погодитись, що це погано. Те, що це говорить про людину, не є темою для цього сайту. (Хоча кожен, хто бажає витратити трохи часу, щоб внести ідею, робить щось правильно, навіть якщо ця ідея не діє)
Позов до Моніки

2
Так, це неправильна відповідь. Але я боюся, що деякі люди можуть прийти до тієї самої ідеї. Через коментар та посилання я що-небудь дізнався, це не є негативними моментами для мене.
Роберт Анджеюк,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.