Is there an easy workaround for this clangd behavior re: pragma once?

Hey all,

This is my first post on this forum so please let me know if I’ve missed anything!

I’m using clangd 12.0.0 on Ubuntu 21.04. I first encountered this using the clangd extension for VSCode but I’ve found a more minimal reproducer since then.

In my toy project I’ve got four files:

File contents

main.cpp

#include "a.hpp"
int main() { return 0; }

a.hpp

#pragma once
inline void f();
inline void g() {}
#include "a.ipp"

a.ipp

#pragma once
#include "a.hpp"
inline void f() {}

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(main)
add_executable(main main.cpp a.hpp a.ipp)

I run the following commands to generate a compile commands database and then run clangd to check a.hpp.

Terminal commands
mkdir build
cmake -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=1 .
clangd --compile-commands-dir=build --check=a.hpp

The error message I get is: E[12:31:09.034] [redefinition] Line 4: in included file: redefinition of 'g', with the full output below:

Full output from clangd
I[12:31:09.016] Ubuntu clangd version 12.0.0-1ubuntu1
I[12:31:09.016] PID: 109416
I[12:31:09.016] Working directory: /home/moss/code/play/clangdpragma
I[12:31:09.016] argv[0]: clangd
I[12:31:09.016] argv[1]: --compile-commands-dir=build
I[12:31:09.016] argv[2]: --check=a.hpp
I[12:31:09.016] Entering check mode (no LSP server)
I[12:31:09.016] Testing on source file /home/moss/code/play/clangdpragma/a.hpp
I[12:31:09.016] Loading compilation database...
I[12:31:09.017] Loaded compilation database from /home/moss/code/play/clangdpragma/build/compile_commands.json
I[12:31:09.017] Compile command from CDB is: /usr/bin/c++ --driver-mode=g++ -c /home/moss/code/play/clangdpragma/a.hpp -fsyntax-only -resource-dir=/usr/lib/llvm-12/lib/clang/12.0.0
I[12:31:09.018] Parsing command...
I[12:31:09.019] internal (cc1) args are: -cc1 -triple x86_64-pc-linux-gnu -fsyntax-only -disable-free -disable-llvm-verifier -discard-value-names -main-file-name a.hpp -mrelocation-model static -mframe-pointer=all -fmath-errno -fno-rounding-math -mconstructor-aliases -munwind-tables -target-cpu x86-64 -tune-cpu generic -fno-split-dwarf-inlining -debugger-tuning=gdb -resource-dir /usr/lib/llvm-12/lib/clang/12.0.0 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/x86_64-linux-gnu/c++/10 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/x86_64-linux-gnu/c++/10 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/backward -internal-isystem /usr/local/include -internal-isystem /usr/lib/llvm-12/lib/clang/12.0.0/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -fdeprecated-macro -fdebug-compilation-dir /home/moss/code/play/clangdpragma/build -ferror-limit 19 -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -faddrsig -x c++-header /home/moss/code/play/clangdpragma/a.hpp
I[12:31:09.019] Building preamble...
I[12:31:09.029] Indexing headers...
I[12:31:09.029] Building AST...
E[12:31:09.034] [redefinition] Line 4: in included file: redefinition of 'g'
I[12:31:09.034] Indexing AST...
I[12:31:09.035] Testing features at each token (may be slow in large files)
E[12:31:09.036] Failed to resolve URI : Scheme must be provided in URI: 
E[12:31:09.036]     tweak: DefineOutline ==> FAIL: Couldn't find a suitable implementation file.
E[12:31:09.036] Failed to resolve URI : Scheme must be provided in URI: 
E[12:31:09.036]     tweak: DefineOutline ==> FAIL: Couldn't find a suitable implementation file.
I[12:31:09.037] All checks completed, 3 errors

As I understand it, this happens because:

  • a.hpp includes a.ipp and a.ipp includes a.hpp
  • both use pragma once (and not include guards)
  • when a.hpp is the target of clangd’s --check, it effectively is treated as the “main file” which means #pragma once is not treated the same way as if the header were being included in, for example, main.cpp above
  • therefore, a.hpp ends up being included twice and that results in the redefinition of void g()

The practical result of this is that when I’m editing a header with this structure in VSCode, there’s always an error indication from clangd on the line where the impl header is included, which is quite distracting. I’m wondering if there’s a way to work around that?

Some ideas I’ve already considered:

  • switch to traditional include guards: works, but it would be difficult to change this within my organization
  • remove the include at the start of the impl header: leads to tons of errors when editing impl header files, because the declarations from the header are no longer visible
  • remove the include at the end of the regular header: no-go, because including the header should always result in an include of the corresponding impl header

I’m not sure it makes sense because I’m fairly new to this tooling, but I would almost like for clangd (or this extension?) to “synthesize” a cpp file including the header, since that’s how it will be encountered in compilation. (Then again, I can also see how all of this could be avoided by just using traditional include guards in the first place!)

Not sure it matters much, but I found it interesting that gcc and clang treat pragma once differently in this situation: Compiler Explorer.

Thanks in advance!

I found a ticket where this issue has come up: "#pragma once" being ignored results in complain about redefined symbol · Issue #377 · clangd/clangd · GitHub

It sounds like you have a pretty good understanding of how the issue arises. I would encourage you to post your explanation (or just a link to this thread) to that ticket, as it’s likely to be helpful if someone attempts to fix it.

I think your suggested idea of synthesizing a cpp file that includes the header makes conceptual sense, though I don’t know how easy it would be to implement, or how well it would play with the preamble optimization when editing the header itself.

1 Like

Thanks @HighCommander4 ! I’ll respond there.