-g

mixed results with more C++ module experimentation

November 22, 2025 C/C++ development and debugging. , , , , ,

Here is some followup from my earlier attempt to use bleeding edge C++ module support.

First experiment: do I need to import all of the std library?

I tried this:

import iostream;

int main() {

  std::cout << "hello world\n";

  return 0;
}

I didn’t know if gcm.cache/std.gcm (already built from my previous experiment) would supply that export, but I get:

g++ -std=c++23 -fmodules   -c -o broken.o broken.cc
In module imported at broken.cc:1:1:
iostream: error: failed to read compiled module: No such file or directory
iostream: note: compiled module file is ‘gcm.cache/iostream.gcm’
iostream: note: imports must be built before being imported
iostream: fatal error: returning to the gate for a mechanical issue
compilation terminated.
make: *** [: broken.o] Error 1

so it appears the answer is no. Also, /usr/include/c++/15/bits/ only appears to have a std.cc, and no iostream.cc:

> find  /usr/include/c++/15/bits/ -name "*.cc"
/usr/include/c++/15/bits/std.cc
/usr/include/c++/15/bits/std.compat.cc

so it appears, for the time being, g++-15 is all or nothing with respect to std imports. However, when using precompiled headers, you usually want a big pre-generated pch that has just about everything, and this is similar, so maybe that’s not so bad (other than namespace pollution.)

Second experiment. Adding a non-std import/export.

I moved a variant of Stroustrup’s collect_lines function into a separate module, like so:

// stuff.cc
export module stuff;
import std;

namespace stuff {

void helper() { std::cout << "call to a private function\n"; }

export
std::vector<std::string> collect_lines(std::istream &is) {

  helper();

  std::unordered_set<std::string> s;
  for (std::string line; std::getline(is, line);)
    s.insert(line);

  //return std::vector<std::string>(s.begin(), s.end());
  return std::vector{std::from_range, s};
}
} // namespace stuff

It turns out that I needed the export keyword on ‘module stuff’, as well as for any function that I wanted to export. Without that I get:

> make 
g++ -std=c++23 -fmodules -c /usr/include/c++/15/bits/std.cc
g++ -std=c++23 -fmodules   -c -o stuff.o stuff.cc
g++ -std=c++23 -fmodules   -c -o try.o try.cc
try.cc: In function ‘int main()’:
try.cc:8:12: error: ‘stuff’ has not been declared
    8 |   auto v = stuff::collect_lines(std::cin);
      |            ^~~~~
make: *** [: try.o] Error 1

The compile error is not very good. It doesn’t complain that collect_lines is not exported, but instead complains that stuff, the namespace itself, is not declared.

I can export the namespace, which is the naive resolution to the compiler diagnostic presented, for example:

export module stuff;
import std;

export namespace stuff {

void helper() { std::cout << "call to a private function\n"; }

//export
std::vector<std::string> collect_lines(std::istream &is) {

  helper();

  std::unordered_set<std::string> s;
  for (std::string line; std::getline(is, line);)
    s.insert(line);

  //return std::vector<std::string>(s.begin(), s.end());
  return std::vector{std::from_range, s};
}
} // namespace stuff

However, that means that the calling code can now call stuff::helper, which was not my intent.

There also does not appear to be any good way to enumerate exports available in the gcm.cache. nm output for the symbol is not any different with or without the export keyword:

> nm stuff.o | grep collect_lines | c++filt
0000000000000028 T stuff::collect_lines@stuff[abi:cxx11](std::basic_istream<char, std::char_traits >&)

This is a critically important tooling failure if modules are going to be used in production. Anybody who has programmed with windows dlls or AIX shared objects, or Linux shared objects with symbol versioning, knows about the resulting hellish nature of the linker error chase, when an export is missed from such an enumeration. Hopefully, there’s some external tool that can enumerate gcm.cache exports. Both grok and chatgpt were unsuccessful advising about tools for this sort of task. The best answer was chatgpt’s recommendation for -fmodule-dump:

> g++ -std=c++23 -fmodules -save-temps -fdump-lang-module   -c -o stuff.o stuff.cc 
fedoravm:/home/peeter/physicsplay/programming/module> ls
broken.cc  gcm.cache  makefile  makefile.clang  std.o  stuff.cc  stuff.cc.002l.module  stuff.ii  stuff.o  stuff.s  try.cc

but that *.module output doesn’t have anything that obviously distinguishes exported vs. non-exported symbols:

> grep -2e stuff::helper -e stuff::collect_lines *.module
Wrote section:28 named-by:'::std::vector<::std::__cxx11::basic_string@std:1<char,::std::char_traits@std:1,::std::allocator@std:1>,::std::allocator<::std::__cxx11::basic_string@std:1<char,::std::char_traits@std:1,::std::allocator@std:1>>>'
Writing section:29 2 depsets
 Depset:0 decl entity:403 function_decl:'::stuff::collect_lines'
 Wrote declaration entity:403 function_decl:'::stuff::collect_lines'
 Depset:1 binding namespace_decl:'::stuff::collect_lines'
Wrote section:29 named-by:'::stuff::collect_lines'
Writing section:30 2 depsets
 Depset:0 decl entity:404 function_decl:'::stuff::helper'
 Wrote declaration entity:404 function_decl:'::stuff::helper'
 Depset:1 binding namespace_decl:'::stuff::helper'
Wrote section:30 named-by:'::stuff::helper'
Writing section:31 4 depsets
 Depset:0 specialization entity:405 type_decl:'::std::__replace_first_arg<::std::allocator<::std::__detail::_Hash_node<::std::__cxx11::basic_string@std:1<char,::std::char_traits@std:1,::std::allocator@std:1>,0x1>>,::std::__cxx11::basic_string@std:1<char,::std::char_traits@std:1,::std::allocator@std:1>>'
--
Writing binding table
 Bindings '::std::operator==' section:8
 Bindings '::stuff::collect_lines' section:29
 Bindings '::stuff::helper' section:30
 Bindings '::std::swap' section:35
Writing pending-entities

Chatgpt summarizes this as follows:

“This is confirmed by overwhelming evidence:

  • GCC bug 113590
  • GCC mailing list discussion July 2024
  • Confirmation from module implementers: “GCC BMIs do not currently record export flags.”

This is intentional (for now): GCC’s binary module interface tracks reachable declarations, not exported ones.”

Trying clang

After considerable experimentation, and both grok and chatgpt help, I was finally able to get a working compile and link sequence using the clang toolchain:

fedoravm:/home/peeter/physicsplay/programming/module> make -f *.clang clean
rm -f *.o *.pcm try
fedoravm:/home/peeter/physicsplay/programming/module> make -f *.clang 
clang++ -std=c++23 -stdlib=libc++ -Wall -Wextra -Wno-reserved-module-identifier --precompile /usr/share/libc++/v1/std.cppm -o std.pcm
clang++ -std=c++23 -stdlib=libc++ -Wall -Wextra -fmodule-file=std=std.pcm --precompile stuff.cppm -o stuff.pcm
clang++ -std=c++23 -stdlib=libc++ -Wall -Wextra -fmodule-file=std=std.pcm -fmodule-file=stuff=stuff.pcm -c try.cc -o try.o
clang++ -std=c++23 -stdlib=libc++ -Wall -Wextra -fmodule-file=std=std.pcm -c stuff.cc -o stuff.o
clang++ -std=c++23 -stdlib=libc++ -Wall -Wextra -fmodule-file=std=std.pcm -fmodule-file=stuff=stuff.pcm try.o stuff.o -o try

Unlike g++, I have to build both the module and the object code for stuff.cc (and facilitated that with a clang.cppm -> clang.cc symlink), but unlike g++, I didn’t need a std.o (for reasons that I don’t understand.)

Dumping the clang-AST appears to be the closest that we can get to enumerating exports. Example:

> clang++ -std=c++23 -stdlib=libc++ -Wall -Wextra -fmodule-file=std=std.pcm  -Xclang -ast-dump -fsyntax-only stuff.cc | less -R

This shows output like:

Screenshot

This is not terribly user friendly, and not something that a typical clang front end user would attempt to do.

This hints that the “way” do dump exports would be to write a clang-AST visitor that dumps all the ExportDecl’s that are encountered (or a complex grep script that attempts to mine the -ast-dump output)

C++ compiler diagnostic gone horribly wrong: error: explicit specialization in non-namespace scope

September 23, 2022 C/C++ development and debugging. , , , , , , , ,

Here is a g++ error message that took me an embarrassingly long time to figure out:

In file included from /home/llvm-project/llvm/lib/IR/Constants.cpp:15:
/home/llvm-project/llvm/lib/IR/LLVMContextImpl.h:447:11: error: explicit specialization in non-namespace scope ‘struct llvm::MDNodeKeyImpl<llvm::DIBasicType>’
 template <> struct MDNodeKeyImpl<DIStringType> {
           ^

This is the code:

template <> struct MDNodeKeyImpl<DIStringType> {
  unsigned Tag;
  MDString *Name;
  Metadata *StringLength;
  Metadata *StringLengthExp;
  Metadata *StringLocationExp;
  uint64_t SizeInBits;
  uint32_t AlignInBits;
  unsigned Encoding;

This specialization isn’t materially different than the one that preceded it:

template <> struct MDNodeKeyImpl<DIBasicType> {
  unsigned Tag;
  MDString *Name;
  MDString *PictureString;
  uint64_t SizeInBits;
  uint32_t AlignInBits;
  unsigned Encoding;
  unsigned Flags;
  Optional<DIBasicType::DecimalInfo> DecimalAttrInfo;

  MDNodeKeyImpl(unsigned Tag, MDString *Name, MDString *PictureString,
               uint64_t SizeInBits, uint32_t AlignInBits, unsigned Encoding,
                unsigned Flags,
                Optional<DIBasicType::DecimalInfo> DecimalAttrInfo)
      : Tag(Tag), Name(Name), PictureString(PictureString),
        SizeInBits(SizeInBits), AlignInBits(AlignInBits), Encoding(Encoding),
        Flags(Flags), DecimalAttrInfo(DecimalAttrInfo) {}
  MDNodeKeyImpl(const DIBasicType *N)
      : Tag(N->getTag()), Name(N->getRawName()), PictureString(N->getRawPictureString()), SizeInBits(N->getSizeInBits()),
        AlignInBits(N->getAlignInBits()), Encoding(N->getEncoding()),
        Flags(N->getFlags(), DecimalAttrInfo(N->getDecimalInfo()) {}

  bool isKeyOf(const DIBasicType *RHS) const {
    return Tag == RHS->getTag() && Name == RHS->getRawName() &&
           PictureString == RHS->getRawPictureString() &&
           SizeInBits == RHS->getSizeInBits() &&
           AlignInBits == RHS->getAlignInBits() &&
           Encoding == RHS->getEncoding() && Flags == RHS->getFlags() &&
           DecimalAttrInfo == RHS->getDecimalInfo();
  }

  unsigned getHashValue() const {
    return hash_combine(Tag, Name, SizeInBits, AlignInBits, Encoding);
  }
};

However, there is an error hiding above it on this line:

        Flags(N->getFlags(), DecimalAttrInfo(N->getDecimalInfo()) {}

i.e. a single missing brace in the initializer for the Flags member, a consequence of a cut and paste during rebase that clobbered that one character, when adding a comma after it.

It turns out that the compiler was giving me a hint that something was wrong before this in the message:

error: explicit specialization in non-namespace scope

as it states that the scope is:

‘struct llvm::MDNodeKeyImpl

which is the previous class definition. Inspection of the code made me think that the scope was ‘namespace llvm {…}’, and I’d gone looking for a rebase error that would have incorrectly terminated that llvm namespace scope. This is a classic example of not paying enough attention to what is in front of you, and going off looking based on hunches instead. I didn’t understand the compiler message, but in retrospect, non-namespace scope meant that something in that scope was incomplete. The compiler wasn’t smart enough to tell me that the previous specialization was completed due to the missing brace, but it did tell me that something was wrong in that previous specialization (which was explicitly named), and I didn’t look at that because of my “what the hell does that mean” reaction to the compilation error message.

In this case, I was building on RHEL8.3, which uses an ancient GCC toolchain. I wonder if newer versions of g++ fare better (i.e.: a message like “possibly unterminated brace on line …” would have been much nicer)? I wasn’t able to try with clang++ as I was building llvm+clang+lldb (V14), and had uninstalled all of the llvm related toolchain to avoid interference.

Debugging with optimization: -g8

May 8, 2015 C/C++ development and debugging. , , , , ,

It was suggested to me to try -g8 for debugging some optimized code (xlC compiler, linuxppcle platform).  Turns out that there is a whole set of -gN options with the xlC compiler that allow various levels of debug instrumentation.

For the optimization bug that I was debugging, -g8 made the problem go away, but with -g5 I was able to break at the nearest case: block and determine that the data in the variables was good coming into that case statement.  Since what appears to come out of that case statement is bad, this isolates the problem significantly.