risorse | campi di bit in C++

Campi di bit in C++11/14

Tutti gli esempi sono stati compilati con la versione 6.1.0 di GCC della distribuzione MinGW curata da nuwen.net con i flag -Wall -Wextra -pedantic -Wsign-promo -std=c++14.

Introduzione

I tipi enumerati che il C++ ha ereditato dal C non consentono di implementare una maschera di bit in modo consistente:

/* file enum-bitfield.cpp */

#include <iostream>

enum Color {
  e_red   = 1,
  e_green = 2,
  e_blue  = 4,
};

int main() {
  Color yellow = e_red | e_green; // error!
  std::cout << "`yellow` code is " << yellow << "\n";
}

Il compilatore genera il seguente errore, in corrispondenza della dichiarazione della variabile yellow:

enum-bitfield.cpp:12:24: error: invalid conversion from 'int' to 'Color' [-fpermissive]

Il problema si può risolvere cambiando il tipo della variabile da Color a int:

...
int main() {
  Color int yellow = e_red | e_green; // ok
  std::cout << "`yellow` code is " << yellow << "\n";
}

/* output:
 *
 * `yellow` code is 3
 */

È però un peccato rinunciare alla tipizzazione della variabile, anche perché ciò può portare alla proliferazione di errori nel codice. È possibile rendere il codice compilabile evitando il declassamento della variabile yellow?

Prima di proseguire, vale la pena notare che l'uso degli enumerati del C ha anche altri risvolti critici. Per esempio, un errore analogo a quello appena discusso si ottiene tentando di passare una combinazione di valori di tipo Color ad una funzione che accetta un parametro del medesimo tipo:

...

void f(Color color) {
  std::cout << "`color` code is " << color << "\n";
}

int main() {
  int yellow = e_red | e_green;
  std::cout << "`yellow` code is " << yellow << "\n";

  f(e_red);           // ok
  f(e_red | e_blue); // error!
}
enum-bitfield.cpp:20:11: error: invalid conversion from 'int' to 'Color' [-fpermissive]

Un aspetto in questo caso non critico, ma che conviene far emergere, è la conversione implicita a intero che subisce del parametro color durante la serializzazione su std::cout in f. Se si compila il codice attivando il flag -Wsign-promo, il compilatore segnala, in corrispondenza di quella linea di codice:

enum-bitfield.cpp:12:38: warning: passing 'Color' chooses 'int' over 'long unsigned int' [-Wsign-promo]

Conviene allora esplicitare la conversione:

...

void f(Color color) {
  std::cout << "`color` code is " << static_cast<int>(color) << "\n";
}
...

Un altro problema degli enumerati C è legato all'impossibilità di usare lo stesso simbolo per due costanti appartenenti a tipi differenti:

...

enum Color {
  e_red   = 1,
  e_green = 2,
  e_blue  = 4,
};

enum Mood {
  e_angry,
  e_blue, // error!
  e_calm,
  // ...
};
...
enum-bitfield.cpp:13:3: error: redeclaration of 'e_blue'

Un ulteriore problema di questa tipologia di dato deriva dal fatto che i valori di un enumerato possono apparire in una qualunque espressione aritmetica:

...

enum Mood {
  e_angry,
  e_blue, // error!
  e_calm,
  // ...
};
...

int main() {
   ...

  f(e_red);
  // f(e_red | e_blue);

  int nonsense = e_red + e_green * e_blue; // compiles!
  std::cout << "`nonsense` code is " << nonsense << "\n";
}

/* output:
 *
 * `yellow` code is 3
 * `color` code is 1
 * `nonsense` code is 9
 */

È consentito pure combinare tra loro costanti di enumerati diversi:

...

enum Permission {
  e_read    = 1,
  e_write   = 2,
  e_execute = 4,
};

int main() {
  ...

  int nonsense = e_red + e_green * e_blue; // compiles!
  std::cout << "`nonsense` code is " << nonsense << "\n";

  int mismatch = e_blue | e_write; // compiles!
  std::cout << "`mismatch` code is " << mismatch << "\n";
}

/* output:
 *
 * `yellow` code is 3
 * `color` code is 1
 * `nonsense` code is 9
 * `mismatch` code is 6
 */

Serve quindi un meccanismo che consenta di combinare tra loro, attraverso gli operatori bitwise, le costanti di un tipo enumerato senza essere costretti a scendere nel dominio degli interi, impedendo tuttavia che queste possano essere usate all'interno di generiche espressioni matematiche alla stregua di normali valori numerici.

Le scoped enumeration

Trasformare l'enumerato Color in un enum class (o equivalentemente a enum struct) è il primo passo per raggiungere l'obiettivo prefissato, dato che le costanti di questo tipo non sono esplicitamente convertibili a un tipo intero:

...

enum class Color {
  e_red   = 1,
  e_green = 2,
  e_blue  = 4,
};

enum class Permission {
  e_read    = 1,
  e_write   = 2,
  e_execute = 4,
};
...

int main() {
  int Color yellow = Color::e_red | Color::e_green; // error!
  std::cout << "`yellow` code is " << static_cast<int>(yellow) << "\n";

  f(Color::e_red);
  // f(Color::e_red | Color::e_blue);

  int Color nonsense = Color::e_red + Color::e_green * Color::e_blue; // error!
  std::cout << "`nonsense` code is " << static_cast<int>(nonsense) << "\n";

  int Color mismatch = Color::e_blue | Permission::e_write; // error!
  std::cout << "`mismatch` code is " << static_cast<int>(mismatch) << "\n";
}

La compilazione della versione aggiornata del codice rileva quattro errori:

enum-bitfield.cpp:22:31: error: no match for 'operator|' (operand types are 'Color' and 'Color')
enum-bitfield.cpp:26:18: error: no match for 'operator|' (operand types are 'Color' and 'Color')
enum-bitfield.cpp:28:50: error: no match for 'operator*' (operand types are 'Color' and 'Color')
enum-bitfield.cpp:31:34: error: no match for 'operator|' (operand types are 'Color' and 'Permission')

Gli ultimi due errori sono graditi: uno segnala un caso d'uso del tipo Color in un contesto non prettamente binario, il secondo una combinazione di costanti di tipo diverso. I primi due errori si risolvono invece introducendo l'operatore mancante:

...

enum class Color {
  e_red   = 1,
  e_green = 2,
  e_blue  = 4,
};

Color operator|(Color lhs, Color rhs) {
  return Color::e_red;
}
...

int main() {
  ...

  Color nonsense = Color::e_red + Color::e_green * Color::e_blue;
  std::cout << "`nonsense` code is " << static_cast<int>(nonsense) << "\n";

  Color mismatch = Color::e_blue | Permission::e_write; // compiles!
  std::cout << "`mismatch` code is " << static_cast<int>(mismatch) << "\n";
}

/* output:
 *
 * `yellow` code is 1
 * `color` code is 1
 * `color` code is 1
 */

Implementazione a parte, il codice risulta così compilabile.

Operatori

Gli operatori bit a bit del tipo enumerato possono essere realizzati tramite gli omologhi operanti sugli interi. Poiché il C++11 permette di ricavare il tipo intero utilizzato dal compilatore per implementare un enumerato tramite la struct underlying_type, non è difficile convertire i due parametri in ingresso nei corrispettivi valori interi – senza rischiare di perdere di precisione! –, effettuare l'operazione richiesta e trasformare il risultato nel tipo enumerato originale:

Color operator|(Color lhs, Color rhs) {
  return Color::e_red;
  using underlying = typename std::underlying_type<Color>::type;
  return static_cast<Color>(
    static_cast<underlying>(lhs) | static_cast<underlying>(rhs));
}
...

/* output:
 *
 * `yellow` code is 3
 * `color` code is 1
 * `color` code is 5
 */

Per evitare duplicazioni di codice, conviene implementare operator| in termini di operator|=:

...

Color& operator|=(Color& lhs, Color rhs) {
  using underlying = typename std::underlying_type<Color>::type;
  return lhs = static_cast<Color>(
    static_cast<underlying>(lhs) | static_cast<underlying>(rhs));
}

Color operator|(Color lhs, Color rhs) {
  using underlying = typename std::underlying_type<Color>::type;
  return static_cast<Color>(
    static_cast<underlying>(lhs) | static_cast<underlying>(rhs));
  return lhs |= rhs;
}

int main() {
  Color yellow = Color::e_red | Color::e_green;
  std::cout << "`yellow` code is " << static_cast<int>(yellow) << "\n";

  Color cyan = Color::e_green;
  cyan |= Color::e_blue;
  std::cout << "`cyan` code is " << static_cast<int>(cyan) << "\n";

  f(Color::e_red);
  f(Color::e_red | Color::e_blue);
}

/* output:
 *
 * `yellow` code is 3
 * `cyan` code is 6
 * `color` code is 1
 * `color` code is 5
 */

L'implementazione di operator|= non è particolarmente efficiente, richiedendo la costruzione di un intero temporaneo, ma il codice più diretto:

  return static_cast<underlying&>(lhs) |= static_cast<underlying>(rhs);

non è valido, perché il C++ non consente di trasformare una referenza non costante – il parametro lhs – a un tipo indipendente. Le implementazioni degli altri operatori binari & e &=, ^ e ^=, e ~ sono del tutto analoghe.

constexpr

Se si dispone di un compilatore che supporta il C++14, gli operatori possono essere dichiarati constexpr, in virtù di [2]:

...

constexpr Color& operator|=(Color& lhs, Color rhs) {
  using underlying = typename std::underlying_type<Color>::type;
  return lhs = static_cast<Color>(
    static_cast<underlying>(lhs) | static_cast<underlying>(rhs));
}

constexpr Color operator|(Color lhs, Color rhs) {
  return lhs |= rhs;
}
...

Generalizzazione

Come generalizzare l'approccio per estenderlo ad un secondo enumerato?

...

int main() {
  ...

  Permission edit = Permission::e_read | Permission::e_write; // error!
  std::cout << "`edit` code is " << static_cast<int>(edit) << "\n";
}
enum-bitfield.cpp:42:40: error: no match for 'operator|' (operand types are 'Permission' and 'Permission')

Per rendere il codice valido si ricorre ai template:

...

template <class T>
constexpr Color T& operator|=(Color T& lhs, Color T rhs) {
  using underlying = typename std::underlying_type<Color T>::type;
  return lhs = static_cast<Color T>(
    static_cast<underlying>(lhs) | static_cast<underlying>(rhs));
}

template <class T>
constexpr Color T operator|(Color T lhs, Color T rhs) {
  return lhs |= rhs;
}
...

/* output:
 *
 * `yellow` code is 3
 * `cyan` code is 6
 * `color` code is 1
 * `color` code is 5
 * `edit` code is 3
 */

Il codice ottenuto è però fin troppo generico: l'implementazione degli operatori binari ora nasconde quella specifica dei tipi che ne definiscono uno proprietario, oltre a fornirne una per i tipi che non dispongono di underlying_type o peggio per i tipi per i quali gli operatori binari non hanno senso:

int main() {
  ...

  std::string a = "one";
  std::string b = "two";
  a |= b;
}
c:\mingw\include\c++\6.1.0\type_traits:2245:38: error: 'std::__cxx11::basic_string<char>' is not an enumeration type
       typedef __underlying_type(_Tp) type;
                                      ^~~~

Per rendere gli operatori disponibili solo per i tipi enumerati scelti dall'utente si può sfruttare lo SFINAE:

...

template <class T>
constexpr T& typename std::enable_if<is_bitfield(T()), T>::type&
operator|=(T& lhs, T rhs) {
  using underlying = typename std::underlying_type<T>::type;
  return lhs = static_cast<T>(
    static_cast<underlying>(lhs) | static_cast<underlying>(rhs));
}

template <class T>
constexpr T typename std::enable_if<is_bitfield(T()), T>::type
operator|(T lhs, T rhs) {
  return lhs |= rhs;
}
...

Il tipo di ritorno degli operatori dipende dal valore ritornato dalla funzione is_bitfield: se true, il valore di ritorno degli operatori saranno T&/T rispettivamente – ottenendo così la definizione originale di entrambi – in caso contrario sarà void&/void, di fatto una substitution failure.

Per consentire al compilatore di generare le versioni degli operatori binari per Color e Permission è dunque necessario fornire una versione apposita della funzione is_bitfield:

...

// types are not bitfields by default
template <class T>
constexpr bool is_bitfield(T) { return false; }

constexpr bool is_bitfield(Color) { return true; }
constexpr bool is_bitfield(Permission) { return true; }

int main() {
 ...
}

Le funzioni sono dichiarate constexpr perché devono essere valutabili a compile-time. Con queste modifiche il codice d'errore relativo all'applicazione dell'operatore |= alle stringhe cambia, a dimostrazione del fatto che il compilatore non tenta più di istanziare l'implementazione generica dell'operatore sul tipo std::string, ma lamenta invece la sua assenza:

enum-bitfield.cpp:57:5: error: no match for 'operator|=' (operand types are 'std::__cxx11::string {aka std::__cxx11::basic_string<char>}' and 'std::__cxx11::string {aka std::__cxx11::basic_string<char>}')
   a |= b;
   ~~^~~~

Namespace

Nell'uso normale, ci si può attendere che il codice di libreria risieda in un namespace diverso da quello che contiene gli enumerati. Il codice continua a funzionare sotto queste condizioni?

#include <iostream>

namespace bitfield {

template <class T>
constexpr bool is_bitfield(T) { return false; }

template <class T>
constexpr typename std::enable_if<is_bitfield(T()), T>::type&
operator|=(T& lhs, T rhs) {
  using underlying = typename std::underlying_type<T>::type;
  return lhs = static_cast<T>(
    static_cast<underlying>(lhs) | static_cast<underlying>(rhs));
}

template <class T>
constexpr typename std::enable_if<is_bitfield(T()), T>::type
operator|(T lhs, T rhs) {
  return lhs |= rhs;
}

} // namespace bitfield

namespace draw {

enum class Color {
  e_red   = 1,
  e_green = 2,
  e_blue  = 4,
};

constexpr bool is_bitfield(Color) { return true; }

void f(Color color) {
  std::cout << "`color` code is " << static_cast<int>(color) << "\n";
}

} // namespace draw

namespace filesystem {

enum class Permission {
  e_read    = 1,
  e_write   = 2,
  e_execute = 4,
};

constexpr bool is_bitfield(Permission) { return true; }

} // namespace filesystem

int main() {
  draw::Color yellow = draw::Color::e_red | draw::Color::e_green;
  std::cout << "`yellow` code is " << static_cast<int>(yellow) << "\n";

  draw::Color cyan = draw::Color::e_green;
  cyan |= draw::Color::e_blue;
  std::cout << "`cyan` code is " << static_cast<int>(cyan) << "\n";

  draw::f(draw::Color::e_red);
  draw::f(draw::Color::e_red | draw::Color::e_blue);

  filesystem::Permission edit =
    filesystem::Permission::e_read | filesystem::Permission::e_write;
  std::cout << "`edit` code is " << static_cast<int>(edit) << "\n";

  std::string a = "one";
  std::string b = "two";
  a |= b;
}
enum-bitfield.cpp:55:43: error: no match for 'operator|' ...
enum-bitfield.cpp:59:11: error: no match for 'operator|=' ...
enum-bitfield.cpp:63:30: error: no match for 'operator|' ...
enum-bitfield.cpp:66:36: error: no match for 'operator|' ...

Chiaramente, le definizioni degli operatori non sono visibili; sfruttando l'argument-dependent lookup[4] è tuttavia facile risolvere il problema: basta includere l'operatore nel namespace che contiene l'enumerato, l'ADL farà il resto:

...

namespace draw {
  ...

using bitfield::operator|;
using bitfield::operator|=;

void f(Color color) {
  std::cout << "`color` code is " << static_cast<int>(color) << "\n";
}

} // namespace draw

namespace filesystem {
  ...

using bitfield::operator|;
using bitfield::operator|=;

} // namespace filesystem
...

/* output:
 *
 * `yellow` code is 3
 * `cyan` code is 6
 * `color` code is 1
 * `color` code is 5
 * `edit` code is 3
 */

Macro

È infine possibile lasciare a una macro l'espansione del codice, considerato che si ripete uguale a sè stesso per ogni enumerato che si vuole trasformare in un bitfield:

...

#define DECLARE_BITFIELD(type_) \
    constexpr bool is_bitfield(type_) { return true; } \
    using bitfield::operator|; \
    using bitfield::operator|=
...

namespace draw {

enum class Color {
  e_red   = 1,
  e_green = 2,
  e_blue  = 4,
};

constexpr bool is_bitfield(Color) { return true; }

using bitfield::operator|;
using bitfield::operator|=;

DECLARE_BITFIELD(Color);

void f(Color color) {
  std::cout << "`color` code is " << static_cast<int>(color) << "\n";
}

} // namespace draw

namespace filesystem {

enum class Permission {
  e_read    = 1,
  e_write   = 2,
  e_execute = 4,
};

constexpr bool is_bitfield(Permission) { return true; }

using bitfield::operator|;
using bitfield::operator|=;

DECLARE_BITFIELD(Permission);

} // namespace filesystem

Sorgenti

Riferimenti

  1. Kalb, J. "Enum BitFields NYC 2016-07-12". YouTube. <https://www.youtube.com/watch?v=cZ3TNrRzHfM>. Visitato il 2 settembre 2016.
  2. Smith, R. "Relaxing constraints on constexpr functions". Standard C++. <https://isocpp.org/files/papers/N3652.html>. Visitato il 2 settembre 2016.
  3. Williams, A. "Using Enum Classes as Bitfields". ACCU. <https://accu.org/index.php/journals/2228>. Visitato il 2 settembre 2016.
  4. "http://en.cppreference.com/w/cpp/language/adl". cppreference.com. <http://en.cppreference.com/w/cpp/language/adl>. Visitato il 5 settembre 2016.

Pagina modificata il 06/09/2016