The following is a somewhat simplified explanation, because most of the classes described below are templated, and I'm omitting such details for brevity.
In MSVC 2017 toolset version 14.16, the std::string
class is derived from _String_alloc
class, which has a single private data member _Mypair
of type _Compressed_pair
. The class _Compressed_pair
is derived from an empty std::allocator<char>
class, and has a single private _Myval2
data member of type _String_val
. The class _String_val
is derived from _Container_base
class, plus it has three private data members: _Bx
of a union
type _Bxty
(which is able to provide either an in-place buffer for small string optimizations or a pointer to the dynamically-allocated buffer for regular strings), _Mysize
and _Myres
, both of size_type
, tracking the length and the reserved storage size for the string.
In release mode, when _ITERATOR_DEBUG_LEVEL
is #define
d as 0
, the _Container_base
is an alias for an empty struct _Container_base0
. However, compiling in debug mode, when _ITERATOR_DEBUG_LEVEL
is not 0
, the _Container_base
is an alias for a non-empty struct _Container_base12
, which contains a single public data member _Myproxy
(which is a pointer to a _Container_proxy
structure).
Now, here's the catch: one of the requirements for the standard layout type is that all of its (non-static) data members shall be declared in the same class (i.e., either all declared in the most derived class itself or all declared in some particular base class, but not scattered among different classes). Thus, because of the aforementioned _Container_base12
helper structure (used for debugging purposes), _String_val
does not satisfy this requirement any more, and therefore cannot be considered being a standard layout type. In particular, its _Myproxy
data member is declared in _Container_base12
, whereas _Bx
, _Mysize
and _Myres
data members are declared directly in _String_val
itself.
By implication, when using a non-zero _ITERATOR_DEBUG_LEVEL
, the std::string
class itself no longer satisfies the standard layout type requirements, since one of the rules is that all (non-static) data members and base classes shall themselves be standard layout types as well.
While this does answer your question about origins of the difference, please bear in mind that these are just undocumented, implementation-specific details, not to be relied upon. Microsoft may change/reorganize their library code in the future. For example, MSVC 2019 toolset version 14.28 already uses composition instead of inheritance: there, the std::string
class is not derived from _String_alloc
anymore, and instead directly contains its own single _Mypair
data member of type _Compressed_pair
. (Fortunately, ABI compatibility between toolsets v14.28 and v14.16 is still preserved this way, it seems, because the resultant memory layout of std::string
remains the same.) And of course the reasoning is similar for MSVC 2019 / toolset v14.28 regarding std::string
not being standard layout in debug mode. However, some future version may bring breaking changes, and then at some point perhaps the std::is_standard_layout<std::string>::value
observed in debug mode may even change to true
without notice.