C макрос для присвоения значения - PullRequest
1 голос
/ 28 января 2020

У меня есть структура вида:

struct foo {
   bool has_value;
   int value;
}

Я хочу создать макрос, который можно использовать для назначения значения следующим образом:

ASSIGN(foo) = 0;
   or
ASSIGN(foo) += 5;

когда макрос:

#define ASSIGN(s) \
    s->has_value = true; s->value

Однако макрос не является , если безопасен. В приведенном ниже примере , если будет «съедать» первую часть макроса ASSIGN.

if (cond)
    ASSIGN(foo) += 5;

Любое предложение о том, как сделать ASSIGN , если безопасно?

Ответы [ 3 ]

3 голосов
/ 28 января 2020

Используйте оператор запятой и добавьте парены для правильной меры.

#define ASSIGN(s) \
    ((s)->has_value = true), (s)->value

В операторе if он расширится до:

if (cond)
    ((foo)->has_value = true), (foo)->value += 5;

Однако , макросы в этом использовании - очень плохая идея.
Вы можете почти всегда придумать оскорбительный код, который сломает ваш макрос.

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


Вот один плохой способ, которым этот макрос сломается:
ASSIGN(foo) = M_PI;
printf("Sin of PI/2 = %f\n", sin(ASSIGN(foo) *= 0.5));

Вы можете ожидать, что для печати 1.
Возможно, он даже не скомпилируется.

1 голос
/ 28 января 2020

Любое предложение о том, как сделать ASSIGN, если это безопасно?

В общем, вы не можете этого сделать. Вы должны не использовать макросы для lvalue. Это ужасная идея, и она, скорее всего, приведет к сумасшествию и невозможности найти ошибки.


Это действительно похоже на XY проблему . В вашем случае, как я понимаю, вы хотите создать макрос, чтобы упростить выражение, которое должно выполняться многократно в вашем коде.

Вместо использования простого макроса с одним аргументом вы можете определить макрос с двумя аргументами. Таким образом, он будет более совместим с остальной частью вашего кода, но при этом достигнет того же результата, используя более согласованный semanti c, который также "if-safe".

Вот оно ( Я называю это ASSIGN_FOO просто чтобы отличить guish от вашего):

#define ASSIGN_FOO(x, v) do { (x)->has_value = true; (x)->value = v; } while (0)

struct foo var;
ASSIGN_FOO(var, 123);

Если вам интересно об этом do { ... } while (0), посмотрите здесь .

Если вы хотите, чтобы макрос возвращал назначенное значение, хотя (как вы ожидаете от обычного назначения), это не очень хороший вариант.

Вместо использования макроса Вы можете определить встроенную функцию, объявив ее с помощью __attribute__ ((always_inline)). Таким образом, компилятор интегрирует код функции непосредственно в вызывающую программу, и функция будет действовать точно как макрос при компиляции программы, за исключением того, что теперь он более мощный, поскольку его можно использовать в большем количестве контекстов.

inline int __attribute__ ((always_inline)) assign_foo(struct foo *x, int value) {
    x->has_value = true;
    x->value = value;
    return x->value;
}

struct foo var;
assign_foo(var, 123);

В дополнение к этому, не имеет особого смысла использовать макрос, который вы определили при обновлении значения в вашей структуре, поскольку он может легко привести к нежелательному неопределенному поведению, например:

struct foo var;
ASSIGN(var) += 5;

Что расширяется до:

var->has_value = true; var->value += 5; // Undefined behavior, var->value used uninitialized!

Решение здесь:

  1. Если вы уже знаете, что значение присутствует, не имеет смысла переназначив has_value = true, вы можете просто сделать приращение напрямую:

    var->value += 10;
    
  2. Если вы не знаете, присутствует ли значение, используйте функцию, чтобы сделать это безопасно вместо этого :

    inline int __attribute__ ((always_inline)) increment_foo(struct foo *x, int value) {
        if (!x->has_value) {
            x->has_value = true;
            x->value = value;
        } else {
            x->value += value;
        }
    
        return x->value;
    }
    
    increment_foo(var, 10);
    

Сравнение:

struct foo x;
printf("%d\n", ASSIGN(x) = 3);    // Compilation error.
printf("%d\n", ASSIGN_FOO(x, 3);  // Compilation error.
printf("%d\n", assign_foo(x, 3)); // No problem.

struct foo y;
printf("%d\n", ASSIGN(y) += 3);           // Compilation error.
ASSIGN(y) += 3; printf("%d\n", y->value); // Undefined behavior.
printf("%d\n", increment_foo(y, 3));      // No problem.
0 голосов
/ 28 января 2020

Такие макрокоманды, нарушающие синтаксис, обычно плохая идея, но вы можете сделать это с помощью

#define ASSIGN(s) ((s).has_value=1,&(s))->value

, если хотите, чтобы оно принимало lvalue, или

#define ASSIGN(sp) ((sp)->has_value=1,(sp))->value

, если хотите. взять указатель.

Рабочий пример (https://godbolt.org/z/fHAGRa):

#include <stdbool.h>
#include <stdio.h>
struct foo {
   bool has_value;
   int value;
};

#define ASSIGN(s) ((s).has_value=1,&(s))->value

int main()
{
    struct foo x;
    ASSIGN(x) = 21;
    printf("%d\n", ASSIGN(x) *= 2 ); //prints 42
    return ASSIGN(x); //returns the last value (42)
}

Он не защищает от двойной оценки - для этого вам понадобится расширение оператора выражения или вспомогательная встроенная функция, такая как

static inline struct foo *set_has_value_return_ptr(struct foo *X){ 
      return X->has_value=1, X; 
}
#define ASSIGN(s) (set_has_value_return_ptr(&(s)))->value

Объяснение:

Наивный ((s).has_value=1,(s).value) или ((s).has_value=1,(s)).value не будет работать из-за lvalue Преобразование в rvalue выполняется оператором запятой, но ((s).has_value=1,&(s))->value или (*((s).has_value=1,&(s))).value будет, потому что разыменование указателя (*) и -> всегда выдают lvalue независимо от того, был ли их аргумент указателя lvalue или rvalue .

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