<< Posts

MSVC doesn't support max_align_t

I ran into a bit of a frustrating problem the other day when attempting to use the max_align_t type in a C project. According to the standard, this type should be supported in a conforming C11 standard library. I am compiling my project with the C17 standard, so it should be there... right in <stddef.h>. The most frustrating part is that this type is defined in <cstddef>! An include that is only accessible in C++...

Here is the entirety of MSVC's <stddef.h>:

(You don't need to read this carefully. It's just to prove that I looked)

//
// stddef.h
//
//      Copyright (c) Microsoft Corporation. All rights reserved.
//
// The C  Standard Library header.
//
#pragma once
#ifndef _INC_STDDEF // include guard for 3rd party interop
#define _INC_STDDEF

#include 

#pragma warning(push)
#pragma warning(disable: _UCRT_DISABLED_WARNINGS)
_UCRT_DISABLE_CLANG_WARNINGS

_CRT_BEGIN_C_HEADER



#ifdef __cplusplus
    namespace std
    {
        typedef decltype(__nullptr) nullptr_t;
    }

    using ::std::nullptr_t;
#endif



#if _CRT_FUNCTIONS_REQUIRED

    _ACRTIMP int* __cdecl _errno(void);
    #define errno (*_errno())

    _ACRTIMP errno_t __cdecl _set_errno(_In_ int _Value);
    _ACRTIMP errno_t __cdecl _get_errno(_Out_ int* _Value);

#endif // _CRT_FUNCTIONS_REQUIRED



#if defined _MSC_VER && !defined _CRT_USE_BUILTIN_OFFSETOF
    #ifdef __cplusplus
        #define offsetof(s,m) ((::size_t)&reinterpret_cast((((s*)0)->m)))
    #else
        #define offsetof(s,m) ((size_t)&(((s*)0)->m))
    #endif
#else
    #define offsetof(s,m) __builtin_offsetof(s,m)
#endif

_ACRTIMP extern unsigned long  __cdecl __threadid(void);
#define _threadid (__threadid())
_ACRTIMP extern uintptr_t __cdecl __threadhandle(void);



_CRT_END_C_HEADER
_UCRT_RESTORE_CLANG_WARNINGS
#pragma warning(pop) // _UCRT_DISABLED_WARNINGS
#endif // _INC_STDDEF

And here is max_align_t defined in <cstddef>


// cstddef standard header (core)

// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

// Omitted for brevity...

_STD_BEGIN
_EXPORT_STD using _CSTD ptrdiff_t;
_EXPORT_STD using _CSTD size_t;
_EXPORT_STD using max_align_t = double; // most aligned type
_EXPORT_STD using nullptr_t   = decltype(nullptr);

// ...

Why do we even need this type?

It's useful to know what the compiler and its associated standard library implementation consider to be the maximum byte alignment. A cast between types is usually only valid when the two types have the same alignment and the address being pointed is properly aligned. What you aren't allowed to do is cast an a pointer that is aligned on a N byte boundary to a type that requires an M byte boundary where M > N.

// Example of a bad situation.
short smallIntegerArray[] = { 1, 2 }; // Could be a aligned on a 2-byte boundary.
int* largerIntegerArray = (int*)smallIntegerArray; // ints are expected to be aligned on a 4-byte boundary.

int firstInteger = largerIntegerArray[0]; // Dereferencing this cast could be bad...

short* recast = (short*)largerIntegerArray;
assert(recast == smallIntegerArray); // Addresses could be different depending on what the compiler did on line 2.

This example is horrible for lots of reasons. First, casting between these two types is undefined behavior. Alignment requirements are probably part of that undefinedness. We are casting a pointer that could be at an address that is 2-byte aligned to a type that requires 4-byte alignedness. So, in a made up address space lets say that smallIntegerArray is stored at address 1022. The address 1022 is 2-byte aligned because 1022 % 2 == 0. When casting this address and interpreting it as an integer pointer we would need the address to be on a 4-byte boundary. So, 1020 or 1024 would work, but not 1022 because 1022 % 4 != 0. We've just messed things up and the compiler may generate accesses to memory that could result in invalid values being retrived or even cause the program to crash on some architectures.

But then, how does something like malloc work? Can't you cast the address received from malloc to any type? Yes! Because malloc ensures that the addresses it returns are aligned to the maximum size allowed by the system. Say that the maximum alignment required by types in the system is 8 bytes. Any address that malloc gives back would be 8-byte aligned (e.g., 1024, 1032, and 1040 in our example address space). You can cast these 8-byte aligned addresses to any stricter byte alignment: 1024 % 8 == 0 && 1024 % 4 == 0 && 1024 % 2 == 0 && 1024 % 1 == 0.

So to come back around to why max_align_t is an important type. If you want to allocate or use addresses that can store any type, its alignment should be equal to the largest alignment required by types in the system. I've been working on some stuff that needs to store types generically in some... interesting ways. So down the rabbit hole I went. I'm hoping this explanation makes sense as it's something I had not thought too much about previously in my C and C++ career.

What to do about our missing max_align_t

While frustrating, the easy solution is to just create my own type similar to max_align_t and special case it for Windows development. I even know what to define it as on Windows. It's right there in <cstddef> plain as day. In MSVC, the most aligned type is a double. However, I did want to share an interesting way of figuring out what would be the largest alignment requirement without looking at MSVC's <cstddef>. We can just create a union of all the types we want to examine. The union will naturally have to fit the largest type specified as well as meet its alignment requirements.

union my_max_align
{
    short dummy0;
    long dummy1;
    double dummy2;
    long double dummy3;
    void* dummy4;
    void (*dummy5)();
};

size_t max_align = alignof(union my_max_align);