Должны ли составные операторы присваивания для перечислений действительно определяться в терминах связанных с ними арифметических операторов? - PullRequest
0 голосов
/ 29 сентября 2019

Обычно при перегрузке операторов арифметические операторы (включая побитовые операторы) определяются в терминах связанных составных операторов присваивания (например, operator+ и operator| обычно полагаются на operator+= иoperator|=).

class SomeClass {
    SomeType data;
    // ...

  public:
    SomeClass& operator+=(const SomeClass& other) {
        data += other.data;
        return *this;
    }
    friend SomeClass operator+(SomeClass l, const SomeClass& r) { return l += r; }

    friend SomeClass& operator|=(SomeClass& l, SomeClass r) {
        l.data |= r.data;
        return l;
    }
    friend SomeClass operator|(SomeClass l, SomeClass r) { return l |= r; }
};

Однако, когда дело доходит до enum с (независимо от того, является ли он незаданным или enum class), логика, как правило, меняется на противоположную, при этом предпочтительным решением часто вместо этогоопределить составные операторы присваивания в терминах арифметических операторов 1 .

using Underlying = uint8_t;

enum class SomeEnum : Underlying {
    A_VALUE   = 0b0000'0001,
    B_VALUE   = 0b0000'0010,
    // ...
    H_VALUE   = 0b1000'0000,

    CLEAR_ALL = 0,
    SET_ALL   = static_cast<uint8_t>(-1),
};

// Arithmetic first.
constexpr SomeEnum operator|(SomeEnum l, SomeEnum r) {
    return static_cast<SomeEnum>(static_cast<Underlying>(l) | static_cast<Underlying>(r));
}
constexpr SomeEnum& operator|=(SomeEnum& l, SomeEnum r) { return l = l | r; }

// Compound assignment first.
constexpr SomeEnum operator+=(SomeEnum& l, SomeEnum r) {
    Underlying ul = static_cast<Underlying>(l);
    ul += static_cast<Underlying>(r);
    return l = static_cast<SomeEnum>(ul);
}
constexpr SomeEnum operator+(SomeEnum l, SomeEnum r) { return l += r; }

1: хотя ответы на вопросы дают примеры того, как operator|= определяется в терминахoperator| и определение operator| в терминах operator|=, при этом оба ответа публикуются в течение получаса друг от друга (и, таким образом, имеют почти одинаковую экспозицию), у первого значительно больше голосов.Это говорит о том, что это наиболее предпочтительное решение на сегодняшний день .


Теперь я вижу причину такого подхода: код заметно чище, если мы перевернем все вокруги сначала определим арифметический оператор, особенно если мы также хотим доступности во время компиляции.Тем не менее, это также идет вразрез с шаблоном, используемым почти (?) Повсюду в другом месте, что заставляет его выглядеть немного подозрительным ИМО (главным образом потому, что оно нарушает принцип наименьшего удивления).

В свете этого я могу 'не может не удивляться: Должны ли перевернуть вещи здесь?Стоит ли преимущество простоты нарушать невысказанную гарантию того, что "a += b в целом более эффективен, чем a + b, и его следует, если возможно, предпочтительнее" 2 ?

Проще говоря,если для перечисления определить операторы 3 , следует , то мы определяем составные операторы в терминах связанных арифметических операторов, а не наоборот, как кажется, общая рекомендация?

2: См. Сноску № 3 к первая ссылка .

3: Обычно побитовые операторы, но я хотел говорить в целом.

1 Ответ

1 голос
/ 29 сентября 2019

Говорить об эффективности здесь неправильно.Любой хороший компилятор скомпилирует их в одно и то же. gcc имеет значение -O1, и я подозреваю, что большинство компиляторов тоже.

Поскольку оптимизация заканчивается тем же:

Underlying ul = static_cast<Underlying>(l);
ul += static_cast<Underlying>(r);
return l = static_cast<SomeEnum>(ul);

Underlying ul;
(ul = static_cast<Underlying>(l)) += static_cast<Underlying>(r);
return l = static_cast<SomeEnum>(ul);

Underlying ul;
return l = static_cast<SomeEnum>((ul = static_cast<Underlying>(l)) += static_cast<Underlying>(r));

return l = static_cast<SomeEnum>(static_cast<Underlying>(l) + static_cast<Underlying>(r));

return l = operator+(l, r);

Это не такИмеет смысл реализовать operator@ для перечислений в терминах operator@=, потому что вы копируете его в его базовый тип, выполняете операцию, а затем копируете ее обратно и назначаете копии.В классе (теоретически) должно быть меньше копий / ходов при использовании A @= B вместо A = A @ B.

...