Today I want to take a look at a piece of C++ code that Copilot generated for me recently. The code here will not be 1:1 the same because originally it was inserted as a part of the bigger code base – I changed the names and simplified it to focus on the key topic, but the overall idea is exactly the same.
At first glance, the generated code looked perfectly reasonable. In fact, it even compiled. The problem is that it also contained a subtle semantic trap that is very easy to miss if you don’t stop and think carefully about what the code is actually meant to do.
The code started with an enum representing permissions where the values are bitmasks:
enum class Permission
{
kNone = 0,
kRead = 1 << 0,
kWrite = 1 << 1,
kExecute = 1 << 2
};And then Copilot generated an associated stream operator to print the enum value using std::cout:
std::ostream& operator<<(std::ostream& os, const Permission p)
{
switch (p) {
case Permission::kNone:
return os << "None";
case Permission::kRead:
return os << "Read";
case Permission::kWrite:
return os << "Write";
case Permission::kExecute:
return os << "Execute";
default:
return os << "UNKNOWN PERMISSION";
}
}So far, everything still looks fine. The next step overloads the bit or operator for the Permission enum:
Permission operator|(const Permission lhs, const Permission rhs)
{
return static_cast<Permission>(static_cast<int>(lhs) | static_cast<int>(rhs));
}Nothing immediately suspicious here either, but the things start to look interesting because Copilot generated also a function that, according to some complex logic, returns combinations of permissions in form of:
Permission GetPermissions()
{
return Permission::A | Permission::B;
}This is where things start getting tricky. The code is not obviously wrong because representing flags as bitmasks is a completely valid technique, but let’s test this with a simple example:
Permission GetPermissions()
{
return Permission::kNone | Permission::kWrite;
}
int main()
{
const Permission p = GetPermissions();
std::cout << "Current permissions: " << p;
}This works perfectly fine because:
00000000 | 00000010 = 00000010And 00000010 corresponds exactly to Permission::kWrite, so the output becomes:
Current permissions: WriteHowever, when the function reaches a path like this:
Permission GetPermissions()
{
return Permission::kRead | Permission::kWrite;
}the output suddenly changes to:
Current permissions: UNKNOWN PERMISSIONHow is this possible that although all enum values of the Permission enum are covered in switch/case statement, after getting the object p of type Permission and printing it, no valid value of the enum is displayed?
The answer is: the value returned from GetPermissions() is simply not represented by any enum field. Take a look:
Permission::kRead = 00000001
Permission::kWrite = 00000010Their bitwise or produces:
00000011But there is no enum field equal to 00000011 and C++ does not forbid such value from existing inside an enum object.
The key problem
This reveals a deeper conceptual conflict:
- enums are designed to represent one value from a closed set
- bitmasks are designed to represent combinations of values
These are fundamentally different concepts and without writing a logic which actually enforces the rules of how these 2 contradicting worlds co-exist, we end up with a subtle bug hidden behind a clean API. Of course, the solution is not to keep extending the enum with values like:
kReadWrite
kWriteExecute
kReadExecutebecause this does not scale. With more flags, the number of combinations grows exponentially. Let’s look at how to fix the issue properly.

AI is powerful. Snippets are instant.
Stop prompting for the same patterns repeatedly. Get almost 100 free VS Code snippets for C++, Python, CMake and Bazel from piko::snippets GitHub repository.
Don’t lie with the function signature
Using enums to represent individual bitmask values is perfectly fine. It improves the usage and the readability because you no longer need to remember whether “read” permission is equal to 1, 2, or 16. But the moment you start combining those values, the result is no longer guaranteed to be a valid Permission enum value, so the API should reflect that reality.
The first step is changing the overloaded | operator – instead of pretending that the result is still a Permission, return an integer mask explicitly:
int operator|(const Permission lhs, const Permission rhs)
{
return static_cast<int>(lhs) | static_cast<int>(rhs);
}Now the operator no longer falsely suggests that combining two permissions produces another valid enum field. Similarly, any function returning combined permissions should return an integer mask instead of Permission:
int GetPermissions()
{
return Permission::kRead | Permission::kWrite;
}
int main()
{
const int p = GetPermissions();
std::cout << "Current permissions: " << p;
}This now produces:
Current permissions: 3The value is no longer pretending to be a single enum value. It is simply a bitmask containing multiple flags what is clearily communicated by the API now. If you want, you can print it in a nicer form by using a helper function – here is an example of a simple one:
void PrintPermissions(const int p)
{
if (p & static_cast<int>(Permission::kRead)) {
std::cout << "Read";
}
if (p & static_cast<int>(Permission::kWrite)) {
std::cout << "Write";
}
if (p & static_cast<int>(Permission::kExecute)) {
std::cout << "Execute";
}
std::cout << std::endl;
}Now the output becomes:
Current permissions: ReadWriteRead also:
- When AI gets C++ bitmask enums almost right
- 10 VS Code snippets essential for every C++ engineer
- Storing your ATTiny program in…EEPROM?
- Bug of the week #12
- Sharing variable between bash scripts
- A 40-line LLM-based bash command executor in Python
- GTest and short-circuit evaluation in C++
- AI is powerful. Snippets are instant.
- From AUTOSAR to S-Core: the first C++ pub/sub implementation
- How to write Arduino Uno code with Python?
- Combining Bazel with Docker
- Running commands with timeout on Linux
- Running Python unit tests with CMake
- Thirdparty dependencies with FetchContent
- Bug of the week #11
- Combining CMake with Docker
- How to search the internet from Linux terminal?
- Folding expressions in C++
- How to derive from an enum in Python?
- Bug of the week #10
- Trying ROS2: client/server within a single container
- Make C++ a better place #4: Go as an alternative
- How to convert hex to dec in Linux terminal?
- Setting up a Python project with CMake
- Separating builds for different configs with Bazel
- Trying ROS2: pub/sub within a single container
- Bug of the week #9
- UDP multicasting with Python
- Destruction order vs thread safety in C++
- Let’s review some code: C++ #2
- Make C++ a better place #3: D as an alternative
- Registering callback using std::function in C++
- Bug of the week #8
- TCP client/server with Python
- Simple menus in Bash scripts with select
- Calling member function on a nullptr in C++
- Bug of the week #7
- Python lru_cache explained
- How to dockerize a Python application?
- Make C++ a better place #2: CppFront as an alternative
- Parameters combinations in GoogleTest
- Data transfer with curl
- Python reduce explained
- Bug of the week #6
- Custom literals in C++
- Linux and hash command
- 5 Python good practices which make life easier
- Let’s review some code: Python #1
- Make C++ a better place #1: What does better mean
- Enums vs enum class in C++
- Bug of the week #5
- UDP client/server with Python
- Hard links in Linux
- Functions calling order in unit tests in C++
- Bug of the week #4
- Yield in Python – state machines, coroutines and more
- Copy files from another branch with Git
- Make C++ a better place #0: Introduction
- 5 misconceptions about std::move in C++
- How to use xargs on Linux?









