link
Debugging할때 나오는 Alingment와 Size 개념을 잡아보자.
std::optional<uint32_t>
Yes. The key is that std::optional<uint32_t> is not the same size as uint32_t.
In your code, the declaration is:
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
};This is exactly the code in your Vulkan program.
If your debugger shows:
graphicsFamily
Alignment: 4 bytes
Size: 8 bytesthat is normal.
1. First: uint32_t itself
A uint32_t normally looks like:
uint32_t graphicsFamily;Its typical layout is:
alignment = 4 bytes
size = 4 bytesbecause:
uint32_t
↓
32 bits
↓
32 / 8
↓
4 bytesSo:
address
│
├── +0 [ 4 bytes ]
│
└── +4For example:
static_assert(sizeof(uint32_t) == 4);
static_assert(alignof(uint32_t) == 4);On your normal 64-bit desktop platform, these will typically be true.
2. But std::optional<uint32_t> needs more information
Now look at:
std::optional<uint32_t>An optional<T> needs to represent two states:
1. There is a uint32_t value.
2. There is no value.For example:
std::optional<uint32_t> x;
x = 5;means:
x contains 5while:
std::optional<uint32_t> x;means:
x does not contain a valueSo internally, the implementation needs something conceptually like:
┌───────────────────────────────┐
│ uint32_t value │
│ │
│ uint32_t has_value flag │
└───────────────────────────────┘Conceptually:
template<class T>
class optional {
T value;
bool engaged;
};This is only a conceptual model, not necessarily the exact implementation used by your standard library.
3. Why isn’t the size 5 bytes?
You might initially expect:
uint32_t = 4 bytes
bool = 1 byte
4 + 1 = 5 bytesBut C++ objects have alignment requirements.
Your uint32_t has:
alignment = 4So the optional<uint32_t> object typically needs to have an overall size that is a multiple of 4.
For example, an implementation could conceptually have:
offset
0 4 5 8
│ │ │ │
▼ ▼ ▼ ▼
┌───────────────────┬─────┬───────┐
│ uint32_t │flag │padding│
│ 4 bytes │1 B │3 B │
└───────────────────┴─────┴───────┘Therefore:
4 bytes value
+ 1 byte state
+ 3 bytes padding
-----------------
= 8 bytesThat’s one common way an optional<uint32_t> can occupy 8 bytes.
4. What does “alignment = 4” mean?
This is an extremely important distinction:
Alignment is not size.
If your debugger says:
graphicsFamily
Alignment: 4
Size: 8it means:
Alignment = 4The object should start at an address appropriate for 4-byte alignment.
while:
Size = 8means:
The object occupies 8 bytes of storage.
For example, it might be located at:
0x1000because:
0x1000 % 4 == 0But the object can still occupy:
0x1000 ... 0x1007which is 8 bytes.
5. Think about a structure
Your structure is:
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
};If each optional<uint32_t> is 8 bytes, the conceptual layout could be:
QueueFamilyIndices
offset 0
│
▼
┌────────────────────────┐
│ graphicsFamily │
│ │ 8 bytes
│ optional<uint32_t> │
└────────────────────────┘
offset 8
│
▼
┌────────────────────────┐
│ presentFamily │
│ │ 8 bytes
│ optional<uint32_t> │
└────────────────────────┘
offset 16So typically:
sizeof(QueueFamilyIndices)would be:
16 bytesand:
alignof(QueueFamilyIndices)would typically be:
4 bytesYou can verify your actual compiler’s result:
#include <cstdint>
#include <iostream>
#include <optional>
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
};
int main() {
std::cout
<< "sizeof(uint32_t): "
<< sizeof(uint32_t) << '\n';
std::cout
<< "alignof(uint32_t): "
<< alignof(uint32_t) << '\n';
std::cout
<< "sizeof(optional<uint32_t>): "
<< sizeof(std::optional<uint32_t>) << '\n';
std::cout
<< "alignof(optional<uint32_t>): "
<< alignof(std::optional<uint32_t>) << '\n';
std::cout
<< "sizeof(QueueFamilyIndices): "
<< sizeof(QueueFamilyIndices) << '\n';
std::cout
<< "alignof(QueueFamilyIndices): "
<< alignof(QueueFamilyIndices) << '\n';
}On a typical 64-bit Linux C++ implementation, you may get:
sizeof(uint32_t): 4
alignof(uint32_t): 4
sizeof(optional<uint32_t>): 8
alignof(optional<uint32_t>): 4
sizeof(QueueFamilyIndices): 16
alignof(QueueFamilyIndices): 4The exact representation is implementation-dependent, so don’t rely on optional<uint32_t> always being exactly 8 bytes on every platform/compiler.
6. Why does Vulkan code use optional here?
This is actually a very nice design.
Your Vulkan code searches for queue families:
if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
indices.graphicsFamily = i;
}and:
if (presentSupport) {
indices.presentFamily = i;
}from your findQueueFamilies() function.
The program initially has:
QueueFamilyIndices indices;which means conceptually:
graphicsFamily = nothing
presentFamily = nothingThen the physical device is examined.
Suppose the GPU has:
Queue family 0
graphics = YES
present = YES
Queue family 1
graphics = YES
present = NOAfter searching:
graphicsFamily = 0
presentFamily = 0So:
indices.graphicsFamily.has_value()is:
trueand:
indices.graphicsFamily.value()returns:
07. Why not just use uint32_t?
You could write:
struct QueueFamilyIndices {
uint32_t graphicsFamily;
uint32_t presentFamily;
};But now you have a problem.
What does this mean before you’ve found the queue family?
graphicsFamily = ???
presentFamily = ???You need some sentinel value:
uint32_t graphicsFamily = UINT32_MAX;Then you might write:
if (graphicsFamily != UINT32_MAX) {
// found
}But std::optional expresses the intent much more clearly:
std::optional<uint32_t> graphicsFamily;means:
"Maybe there is a queue family index."8. optional is essentially a tagged value
A useful mental model is:
std::optional<uint32_t>
┌──────────────────────┐
│ │
│ uint32_t │
│ │
├──────────────────────┤
│ │
│ engaged / empty │
│ │
└──────────────────────┘The second part is effectively a state indicator.
You can think of it as:
optional<uint32_t>
state
│
├── EMPTY
│
└── VALUE
│
└── uint32_tTherefore optional<T> generally requires enough storage for:
T
+
some representation of "has value"
+
possible padding9. Why has_value() works
Your code:
bool isComplete() {
return graphicsFamily.has_value() &&
presentFamily.has_value();
}is asking:
Does graphicsFamily contain a value?
AND
Does presentFamily contain a value?So:
graphicsFamily presentFamily
│ │
▼ ▼
has value? has value?
│ │
└────────┬─────────┘
▼
complete?This is then used here:
return indices.isComplete() &&
extensionsSupported &&
swapChainAdequate;from your Vulkan device-selection code.
10. What happens when you call .value()?
Later your program does:
indices.graphicsFamily.value()and:
indices.presentFamily.value()when creating the logical device.
.value() means:
“Give me the contained
uint32_t.”
But if the optional is empty:
std::optional<uint32_t> x;
x.value(); // throws std::bad_optional_accessThat’s why the program first establishes:
indices.isComplete()before using .value().
11. There is also operator*
Instead of:
indices.graphicsFamily.value()you can write:
*indices.graphicsFamilywhen you know it contains a value.
And:
indices.graphicsFamily.has_value()is equivalent in intent to checking whether the optional is engaged.
You can also use:
if (indices.graphicsFamily) {
std::cout << *indices.graphicsFamily;
}because std::optional has a boolean conversion.
12. The memory layout is the key to your question
So if your debugger reports:
graphicsFamily
Alignment: 4 bytes
Size: 8 bytesdon’t interpret it as:
4 + 8 = 12Instead:
Alignment
↓
how the object should be positioned
Size
↓
how much memory the object occupiesFor a typical implementation:
std::optional<uint32_t>
alignment = 4
size = 8A conceptual layout is:
8 bytes total
┌───────────────────┐
offset │ uint32_t │
0 │ │
│ 4 bytes │
├───────────────────┤
offset │ engaged flag │
4 │ 1 byte │
├───────────────────┤
offset │ padding │
5 │ 3 bytes │
└───────────────────┘Again, this is a conceptual representation, not a guarantee about the standard-library implementation.
The most important takeaway
Your declaration:
std::optional<uint32_t> graphicsFamily;does not mean “an 8-byte integer.”
It means:
optional<uint32_t>
│
├── uint32_t value → 4 bytes
│
└── whether value exists → extra storage
↓
paddingTherefore, on your implementation:
alignment size
uint32_t 4 4
optional<uint32_t> 4 8And the reason Vulkan uses optional<uint32_t> here is semantic rather than performance-related: a queue-family index may or may not have been found yet. The optional makes that state explicit instead of inventing a special sentinel integer.