0

The code

Here's this seemingly innocent, few lines of code.

#include <iostream>
#include <string>
#include <unordered_map>

struct Tag
{
    using name_t        = std::string;
    using tag_map_t     = std::unordered_map<name_t, Tag>;

    name_t              name;
    tag_map_t           children = {};
};

int main(void)
{
    auto foo = Tag{"foo"};
    auto bar = Tag{"bar"};
    foo.children["bar"] = bar;
    std::cout << foo.name << " -> " << foo.children["bar"].name << std::endl;

    return 0;
}

Let's try to compile it

clang

λ clang++ --version
clang version 11.0.1
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: C:\LLVM\bin

λ clang++ -std=c++14 -Weverything -Werror -Wno-c++98-compat tag.cpp -o tag.exe && tag.exe
foo -> bar

msvc++

λ cl
Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27031.1 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

λ vcvars64.bat && cl.exe /std:c++14 /W4 /WX /EHsc tag.cpp /Fe:tag.exe && tag.exe
**********************************************************************
** Visual Studio 2017 Developer Command Prompt v15.9.13
** Copyright (c) 2017 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x64'
Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27031.1 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

tag.cpp
Microsoft (R) Incremental Linker Version 14.16.27031.1
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:tag.exe
tag.obj
foo -> bar

gcc

λ gcc --version
gcc (GCC) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

λ g++ -std=c++14 -pedantic -Wall tag.cpp -o tag.exe && tag.exe
In file included from /usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/stl_algobase.h:64,
                 from /usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/char_traits.h:39,
                 from /usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/ios:40,
                 from /usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/ostream:38,
                 from /usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/iostream:39,
                 from tag.cpp:1:
/usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/stl_pair.h: In instantiation of ‘struct std::pair<const std::basic_string<char>, Tag>’:
/usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/ext/aligned_buffer.h:91:28:   required from ‘struct __gnu_cxx::__aligned_buffer<std::pair<const std::basic_string<char>, Tag> >’
/usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/hashtable_policy.h:233:43:   required from ‘struct std::__detail::_Hash_node_value_base<std::pair<const std::basic_string<char>, Tag> >’
/usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/hashtable_policy.h:264:12:   required from ‘struct std::__detail::_Hash_node<std::pair<const std::basic_string<char>, Tag>, true>’
/usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/hashtable_policy.h:1973:13:   required from ‘struct std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<const std::basic_string<char>, Tag>, true> > >’/usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/hashtable.h:173:11:   required from ‘class std::_Hashtable<std::basic_string<char>, std::pair<const std::basic_string<char>, Tag>, std::allocator<std::pair<const std::basic_string<char>, Tag> >, std::__detail::_Select1st, std::equal_to<std::basic_string<char> >, std::hash<std::basic_string<char> >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >’
/usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/unordered_map.h:105:18:   required from ‘class std::unordered_map<std::basic_string<char>, Tag>’
tag.cpp:11:37:   required from here
/usr/lib/gcc/x86_64-pc-msys/10.2.0/include/c++/bits/stl_pair.h:218:11: error: ‘std::pair<_T1, _T2>::second’ has incomplete type
  218 |       _T2 second;                ///< The second member
      |           ^~~~~~
tag.cpp:5:8: note: forward declaration of ‘struct Tag’
    5 | struct Tag
      |        ^~~

I'm pretty sure gcc is right and I don't understand how clang and visual studio can compile this and turn it into a binary that can even run.

How do they know the size of Tag? Tag is a recursive data structure, right? So somehow these compilers can figure out the size of Tag even though I haven't stored like a shared_ptr in the unordered_map, but the Tag itself. Is this valid C++ code?

Why I haven't even needed a forward declaration for the struct? I thought in C++ the compiler needs to "see" at least a declaration for a name to take it as a valid name, so I'm surprised this does not seem to be the case here. At least according to clang and visual studio. What important detail did I miss here?

Fixing this code to make it work on gcc is really simple, so my question is really about how it can work with the other two compilers.

1 Answers1

5

Template types can be complete with incomplete arguments.

struct incomplete;
template<class T>struct tag_t{using type=T;};

then tag_t<incomplete> is a complete type. Its sizeof is 1 and it is an empty type.

This is not special to templates; incomplete* is also complete, as is incomplete(*)().

Almost all std containers size does not depend on the size of their template parameters; array is the obvious exception.

In practice a high quality container can be written with an incomplete type until you tried to do certain operations that require the type to be complete, like put data in it.

The incompleteness allowed by the standard became more universal with later standard revisions. Probably your standard library for gcc is just less modern.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • The answer here suggests that instantiating a template with an incomplete type is actually undefined behaviour: https://stackoverflow.com/questions/31345193/how-can-an-incomplete-type-be-used-as-a-template-parameter-to-vector-here. – István Siroki Mar 09 '21 at 00:52
  • @istv Instantiating specific templates can be instantiating other templates is not. Some versions of the c++ standard required vector be instantiated with a complete type; violating that makes your program ill formed. Later versions changed that rule if I recall correctly. But incomplete types passed to templates causing UB or Ill formed programs is not a rule; violating requirements of std templates, or other operations like violating ODR, can cause ill formed programs. Is dereferencing a pointer UB? It depends on the pointer, dontit? – Yakk - Adam Nevraumont Mar 09 '21 at 01:34