Понимание _Generic C11

Стандарт C17 требует, чтобы в выражении _Generic:

Никакие две родовые ассоциации в одной и той же родовой селекции не должны укажите совместимые типы. Тип управляющего выражения: тип выражения, как если бы оно подверглось преобразованию lvalue [....]

@Davilsor в этом посте: https://codereview.stackexchange.com/a/289337/265278 утверждает, что:

_Generic на самом деле предназначен только для того, чтобы сделать реализацию <math.h> более переносимой, поэтому я не полностью доверяю ему целочисленные типы. Я бы особенно опасался расширения char и short int до int.

Если uint64_t — это typedef, скажем, unsigned long long int, и они оба у меня есть в каком-то выражении _Generic, будет ли это противоречить «Никакие две универсальные ассоциации в одном и том же общем выборе не должны указывать совместимые типы»? Если да, то какого поведения мне следует ожидать? Компиляция не удалась? Неопределенное поведение? Диагностика?

Возьмем это, например:

#define map(count, iter, func) _Generic (*iter,   \
                                char: map_c,    \
                                unsigned char: map_uc,  \
                                short int: map_si,   \
                                unsigned short int: map_usi,    \
                                int: map_i, \
                                unsigned int: map_ui,   \
                                long int: map_li,   \
                                unsigned long int: map_uli, \
                                long long int: map_lli,     \
                                unsigned long long int: map_ulli,   \
                                float: map_f,   \
                                double: map_d,  \
                                long double: map_ld,    \
                                _Bool: map_b,    \
                                char *: map_s   \
                            )(count, iter, func)

Есть ли что-нибудь, что потенциально может пойти не так в приведенном выше выражении _Generic, особенно если я добавлю к нему целочисленные типы фиксированной ширины из stdint.h?

Существует три типа char, два из которых имеют одинаковый диапазон. Но это имеет отношение к вашему вопросу.

Jonathan Leffler 26.05.2024 15:48

@JonathanLeffler В «doc/generic11.c» вашего репозитория «soq» вы использовали (x) + (y) в качестве управляющего выражения. Но не будет ли это иметь неопределенное поведение для типов signed при переполнении? Если да, то есть ли способ избежать этого?

Harith 26.05.2024 17:21

Да, как и почти в любом выражении, включающем целочисленные типы со знаком, существует риск переполнения. Вы справляетесь с этим так же, как с риском с помощью int z = x+ y; — вы можете игнорировать его, использовать проверяемые арифметические функции из C23 или выполнять другие проверки, если считаете это целесообразным.

Jonathan Leffler 26.05.2024 18:12

@JonathanLeffler Знаете ли вы, реализованы ли «проверенные арифметические функции» C23 какой-либо стандартной библиотекой? Я не видел компилятора, поддерживающего их.

Lundin 27.05.2024 10:55

@Lundin gcc.gnu.org/gcc-14/changes.html говорит, что GCC 14 реализовал <stdckdint.h>.

Harith 27.05.2024 10:57

@Lundin Releases.llvm.org/18.1.0/tools/clang/docs/ReleaseNotes.html говорит, что в Clang 18.1 также реализован <stdckdint.h>.

Harith 05.06.2024 17:06

@Джонатан Я построил ваш общий макрос и в итоге получил тот, который работает для всех целочисленных типов (включая _Bool), работает со смешанными знаковыми и беззнаковыми типами и не имеет целочисленного переполнения. Думаю, вам будет интересно это увидеть: codereview.stackexchange.com/questions/292409/…

Harith 06.06.2024 17:08

Обратите внимание, что §6.5.1.1 Общий выбор ¶3 гласит: Управляющее выражение общего выбора не оценивается. Поскольку оно не оценивается, оно не может переполниться. Это имеет смысл. Управляющее выражение определяет тип во время компиляции; переполнение происходит во время выполнения.

Jonathan Leffler 08.06.2024 04:10

@JonathanLeffler Понятно, так что добавление никогда не было проблемой. _Generic имеет много правил, но мне было очень полезно. Спасибо.

Harith 08.06.2024 04:13

Поскольку мое имя проверено, я узнал несколько хороших ответов. C17 изменил формулировку о том, какие преобразования претерпевает тип управляющего выражения, и я не был в курсе.

Davislor 10.07.2024 17:37
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
10
156
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Если uint64_t является определением типа, скажем, unsigned long long int, и они оба у меня есть в каком-то выражении _Generic, будет ли это противоречить «Никакие две универсальные ассоциации в одном и том же общем выборе не должны указывать совместимые типы»?

Да.

Если да, то какого поведения мне следует ожидать? Компиляция не удалась? Неопределенное поведение? Диагностика?

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

Есть ли что-то, что потенциально может пойти не так в приведенном выше выражении _Generic,

Нет, не так, как сейчас написано.

особенно если я добавлю к нему целочисленные типы фиксированной ширины из stdint.h?

Да, тогда у вас возникнут проблемы, поскольку вы укажете один и тот же тип более одного раза, и вы получите диагностические сообщения, и программа, скорее всего, не сможет скомпилироваться.

«у вас возникнут проблемы, поскольку тогда вы будете указывать один и тот же тип более одного раза». Если мы говорим о типах intn_t/uintn_t - так называемых фиксированной ширины - тогда они по определению не получат один и тот же тип дважды. Все типы от int8_t до int64_t будут уникальными типами. Так что все будет работать нормально. Но в случае int_leastn_t/int_fastn_t несколько типов в списке ассоциаций _Generic могут конфликтовать.

Lundin 27.05.2024 08:41

@Lundin OP спрашивает, что произойдет при добавлении типов фиксированной ширины в список, который уже охватывает основные типы.

Ted Lyngmo 27.05.2024 09:32

Ах, ладно, справедливо, да, тогда определенно будут столкновения.

Lundin 27.05.2024 10:18

@Тед Люнгмо хорошо отвечает на явные вопросы ОП.

Тем не менее, остается без ответа подразумеваемый вопрос: как использовать _Generic для управления кодом, когда uint64_t не соответствует ни одному стандартному типу?

Используйте больше уровней

На верхнем уровне map() добавьте default:, который управляет кодом между uint64_t, uint32_t и т. д. Это можно даже перенести на дополнительные уровни для size_t, uintmax_t, которые могут конфликтовать с предыдущими типами.

#include <stdint.h>
#include <stdio.h>

int map_uc(int, unsigned char*, int);
int map_usi(int, unsigned short*, int);
int map_ui(int, unsigned*, int);
int map_uli(int, unsigned long*, int);
int map_ulli(int, unsigned long long*, int);
int map_u8(int, uint8_t*, int);
int map_u16(int, uint16_t*, int);
int map_u32(int, uint32_t*, int);
int map_u64(int, uint64_t*, int);

#define map2(iter) _Generic (*(iter),   \
    uint8_t: map_u8,  \
    uint16_t: map_u16,  \
    uint32_t: map_u32,  \
    uint64_t: map_u64    \
    )

#define map(count, iter, func) _Generic (*(iter),   \
    unsigned char: map_uc,  \
    unsigned short int: map_usi,    \
    unsigned int: map_ui,   \
    unsigned long int: map_uli, \
    unsigned long long int: map_ulli,   \
    default: map2(iter)   \
    ) ((count), (iter), (func))

int main(void) {
  uint64_t sz = 123;
  uint64_t *xx = &sz;
  printf("%d\n", map(0, xx, 0));
  return 0;
}

Что касается целочисленных типов, _Generic и stdint.h, похоже, существует несколько заблуждений.

  • _Generic не продвигает по типу первый элемент в списке ассоциаций (но проводит его через «преобразование lvalue», что означает отбрасывание квалификаторов типа).

    Однако имейте в виду, что если вы передадите ему выражение, то это выражение само по себе может содержать неявные повышения типа. Например _Generic((signed char){}, ... приведет к signed char но _Generic(+(signed char){}, ... приведет к int. Что, конечно, не имеет ничего общего с _Generic само по себе.

  • Для стандартных «примитивных» целочисленных типов данных C все они будут уникальными типами по отношению друг к другу: signed char, short, int, long и long long. Несмотря на то, что некоторые из них могут иметь одинаковый размер, они будут иметь разный рейтинг конверсии, поэтому _Generic следует рассматривать их все как отдельные типы.

  • Для типов stdint.h они будут реализованы как typedef, соответствующие «примитивным» типам данных (C17 7.20 §4), что означает, что вы не можете иметь, например, int и int16_t в одном и том же списке _Generic ассоциаций, при переносимости. Они могут быть или не быть одного и того же типа.

  • Любой компилятор C обязательно предоставляет int8_t, int16_t, int32_t и int64_t, если соответствующий тип существует на аппаратном уровне целевой системы. Это типы точной ширины, поэтому все они являются уникальными типами по отношению друг к другу, учитывая, что тип существует в конкретной системе.

  • Однако обязательные типы int_leastn_t и int_fastn_t могут быть дубликатами друг друга.

Краткое содержание:

Хорошо определяемый и 100% портативный без поднятой диагностики:

_Generic((x), 
  signed char: 0,
  short: 0,
  int: 0,
  long: 0,
  long long: 0)

_Generic((x), 
  int8_t:  0,
  int16_t: 0,
  int32_t: 0,
  int64_t: 0)

(Хотя малоизвестные/экзотические системы не обязательно должны предоставлять все типы точной ширины stdint.h.)

Другие вопросы по теме