Repository: root-project/root
Pull request #18674: [meta] Properly handle _Atomic type specifier
Author: login: dpiparo
Created: 2025-05-09T11:15:28Z | Merged: 2025-05-14T05:03:34Z

# This Pull request:

## Changes or fixes:
Updates the name normalisation routines in order to properly take into accoung the C11 atomic type specifier `_Atomic`. In absence of this treatment, ill formed type names end up in the lexer of Clang, causing an infinite recursion.

## Checklist:

- [v] tested changes locally
- [v] updated the docs (if necessary)

This PR fixes #18643



Linked issue #18643: [C++23][macOS] Failing test `roottest-root-io-tclass-make` with C++23
### Check duplicate issues.

- [x] Checked for duplicates

### Description

Enabling C++23 resulted in the failure of the following test: https://github.com/root-project/root/actions/runs/14597245841/job/40946206187#step:6:14473

### Reproducer

A minimal reproduce with C++23 in ROOT - killed (SIGKILL):

```C++
void reproducer() {
   TClass::GetClass("TClass")->BuildRealData();
}
```

Running this on mac-beta outputs:
`zsh: killed     bin/root.exe reproducer.C`

There is no issue with C++20

### ROOT version

ROOT master

### Installation method

build from source

### Operating system

MacOS Beta XCode 16.4beta1 and 15.4+XCode 16.3

### Additional context

Upon preliminary investigation, it seems to be an issue with the data member in TClass - `std::atomic<TClass**> fPersistentRef`
Linked issue comments: user: login: dpiparo
created_at: 2025-05-07T20:53:01Z
body: More information. Running this program (the same code above, nothing new)
```
#include "TClass.h"

int main(){
   const auto clname = "TClass";
   TClass::GetClass(clname)->BuildRealData();
   return 0;
}
```
one gets on macOS 15.4 a frozen system. Upon interruption, one gets this stack (focus on frame #20)
```
  * frame #0: 0x000000011572f4f0 libCling.so`clang::Preprocessor::HandleEndOfFile(clang::Token&, bool) + 100
    frame #1: 0x00000001156ecb28 libCling.so`clang::Lexer::LexEndOfFile(clang::Token&, char const*) + 964
    frame #2: 0x00000001156f0480 libCling.so`clang::Lexer::LexTokenInternal(clang::Token&, bool) + 8376
    frame #3: 0x00000001156ec6f4 libCling.so`clang::Lexer::Lex(clang::Token&) + 80
    frame #4: 0x0000000115756ad4 libCling.so`clang::Preprocessor::Lex(clang::Token&) + 52
    frame #5: 0x0000000115736594 libCling.so`clang::Preprocessor::ReadMacroCallArgumentList(clang::Token&, clang::MacroInfo*, clang::SourceLocation&) + 272
    frame #6: 0x0000000115734f2c libCling.so`clang::Preprocessor::HandleMacroExpandedIdentifier(clang::Token&, clang::MacroDefinition const&) + 464
    frame #7: 0x00000001157566d0 libCling.so`clang::Preprocessor::HandleIdentifier(clang::Token&) + 516
    frame #8: 0x00000001156effcc libCling.so`clang::Lexer::LexTokenInternal(clang::Token&, bool) + 7172
    frame #9: 0x00000001156ec6f4 libCling.so`clang::Lexer::Lex(clang::Token&) + 80
    frame #10: 0x0000000115756ad4 libCling.so`clang::Preprocessor::Lex(clang::Token&) + 52
    frame #11: 0x000000011285e774 libCling.so`cling::StartParsingRAII::prepareForParsing(llvm::StringRef, llvm::StringRef, cling::LookupHelper::DiagSetting) + 1564
    frame #12: 0x0000000112862a38 libCling.so`cling::StartParsingRAII::StartParsingRAII(cling::LookupHelper&, llvm::StringRef, llvm::StringRef, cling::LookupHelper::DiagSetting) + 252
    frame #13: 0x000000011285f9c4 libCling.so`cling::LookupHelper::findScope(llvm::StringRef, cling::LookupHelper::DiagSetting, clang::Type const**, bool) const + 736
    frame #14: 0x0000000112516c38 libCling.so`GetClassSharedLibsForModule(cls="_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >", LH=0x0000600001c41e60, skipCore=true) at TCling.cxx:7037:27
    frame #15: 0x00000001124f2604 libCling.so`TCling::GetClassSharedLibs(this=0x000000011f7062b0, cls="_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >", skipCore=true) at TCling.cxx:7149:26
    frame #16: 0x00000001124f4a0c libCling.so`TClingLookupHelper__ExistingTypeCheck(tname="_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >", result="") at TCling.cxx:990:16
    frame #17: 0x00000001122a4404 libCling.so`ROOT::TMetaUtils::TClingLookupHelper::ExistingTypeCheck(this=0x0000600002de3600, tname="_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >", result="") at TClingUtils.cxx:548:35
    frame #18: 0x0000000100b722e4 libCore.so`TClassEdit::GetNormalizedName(norm_name="_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >", name="_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >") at TClassEdit.cxx:906:31
    frame #19: 0x0000000100b9b73c libCore.so`TClass::GetClass(name="_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >", load=true, silent=true, hint_pair_offset=0, hint_pair_size=0) at TClass.cxx:3178:10
    frame #20: 0x0000000100b8ea44 libCore.so`TClass::GetClass(name="_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >", load=true, silent=true) at TClass.cxx:3075:11
    frame #21: 0x0000000100b8efe0 libCore.so`TBuildRealData::Inspect(this=0x000000016fdfc428, cl=0x000000013eb0e2b0, pname="", mname="__a_value", add=0x0000000000000000, isTransient=true) at TClass.cxx:923:28
    frame #22: 0x0000000112502da8 libCling.so`TCling::InspectMembers(this=0x000000011f7062b0, insp=0x000000016fdfc428, obj=0x0000000000000000, cl=0x000000013eb0e2b0, isTransient=true) at TCling.cxx:2968:12
    frame #23: 0x0000000100b97000 libCore.so`TClass::CallShowMembers(this=0x000000013eb0e2b0, obj=0x0000000000000000, insp=0x000000016fdfc428, isTransient=true) const at TClass.cxx:2330:24
    frame #24: 0x0000000112503568 libCling.so`TCling::InspectMembers(this=0x000000011f7062b0, insp=0x000000016fdfc428, obj=0x0000000000000000, cl=0x000000013eb0e090, isTransient=true) at TCling.cxx:3083:18
    frame #25: 0x0000000100b97000 libCore.so`TClass::CallShowMembers(this=0x000000013eb0e090, obj=0x0000000000000000, insp=0x000000016fdfc428, isTransient=true) const at TClass.cxx:2330:24
    frame #26: 0x0000000100b8f890 libCore.so`TClass::BuildRealData(this=0x000000013eb0e090, pointer=0x0000000000000000, isTransient=true) at TClass.cxx:2185:11
    frame #27: 0x0000000100b8f1dc libCore.so`TBuildRealData::Inspect(this=0x000000016fdfda18, cl=0x000000013eb0d340, pname="", mname="__a_", add=0x0000000000000000, isTransient=true) at TClass.cxx:959:28
    frame #28: 0x0000000112502da8 libCling.so`TCling::InspectMembers(this=0x000000011f7062b0, insp=0x000000016fdfda18, obj=0x0000000000000000, cl=0x000000013eb0d340, isTransient=true) at TCling.cxx:2968:12
    frame #29: 0x0000000100b97000 libCore.so`TClass::CallShowMembers(this=0x000000013eb0d340, obj=0x0000000000000000, insp=0x000000016fdfda18, isTransient=true) const at TClass.cxx:2330:24
    frame #30: 0x0000000112503568 libCling.so`TCling::InspectMembers(this=0x000000011f7062b0, insp=0x000000016fdfda18, obj=0x0000000000000000, cl=0x000000013eb0d120, isTransient=true) at TCling.cxx:3083:18
    frame #31: 0x0000000100b97000 libCore.so`TClass::CallShowMembers(this=0x000000013eb0d120, obj=0x0000000000000000, insp=0x000000016fdfda18, isTransient=true) const at TClass.cxx:2330:24
    frame #32: 0x0000000100b8f890 libCore.so`TClass::BuildRealData(this=0x000000013eb0d120, pointer=0x0000000000000000, isTransient=true) at TClass.cxx:2185:11
    frame #33: 0x0000000100b8f1dc libCore.so`TBuildRealData::Inspect(this=0x000000016fdfe6e8, cl=0x00000001051893e0, pname="", mname="fConversionStreamerInfo", add=0x0000000000000060, isTransient=true) at TClass.cxx:959:28
    frame #34: 0x0000000112502da8 libCling.so`TCling::InspectMembers(this=0x000000011f7062b0, insp=0x000000016fdfe6e8, obj=0x0000000000000000, cl=0x00000001051893e0, isTransient=true) at TCling.cxx:2968:12
    frame #35: 0x0000000100b97000 libCore.so`TClass::CallShowMembers(this=0x00000001051893e0, obj=0x0000000000000000, insp=0x000000016fdfe6e8, isTransient=true) const at TClass.cxx:2330:24
    frame #36: 0x0000000100b8f890 libCore.so`TClass::BuildRealData(this=0x00000001051893e0, pointer=0x0000000000000000, isTransient=true) at TClass.cxx:2185:11
    frame #37: 0x0000000100000a40 a`main at a.cpp:6:30
    frame #38: 0x000000019c7aab4c dyld`start + 6000
```
The name `_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >` appears, but it's not a C++ name.

This line causes the system to freeze when using C++23 and not when using C++20(!):
```
TClass::GetClass("_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >")
``` 
or even
```
std::string s;
TClassEdit::GetNormalizedName(s, "_Atomic(map<string,TObjArray*,less<string>,allocator<pair<const string,TObjArray*> > >")
```
or 
```
TClassEdit::GetNormalizedName(s, "_Atomic(map<")
```




author_association: MEMBER
user: login: pcanal
created_at: 2025-05-07T21:43:30Z
body: Why is there a paranthesis in what is supposed to be a typename?
Does closing the paranthesis 'prevent' the crash?
author_association: MEMBER
user: login: dpiparo
created_at: 2025-05-08T05:54:36Z
body: > Why is there a paranthesis in what is supposed to be a typename? 

This is coming out of ROOT's naming routines (see the stack)

> Does closing the paranthesis 'prevent' the crash?

~~Not yet tried.~~ Yes, it does

author_association: MEMBER
user: login: dpiparo
created_at: 2025-05-08T08:10:39Z
body: Hopefully an even simpler reproducer:
```
    const auto clDef = "template <typename T> class myTemplate{}; class myClass{std::atomic<myTemplate<int>* > fMyMember;};";
    gInterpreter->Declare(clDef);
    const auto name = "myClass";
    auto c = TClass::GetClass(name);
    // This hangs with C++23
    c->BuildRealData();
```
Observation: the type of the data member has to be a template. I could not reproduce the problem otherwise.
author_association: MEMBER
user: login: ferdymercury
created_at: 2025-05-08T11:42:19Z
body: Not sure if the freeze could be related to: https://github.com/llvm/llvm-project/issues/29757
author_association: COLLABORATOR
user: login: ferdymercury
created_at: 2025-05-09T13:05:35Z
body: Side note: This totally silly statement also leads to an infinite loop in clang:
```
std::string n; TClassEdit::GetNormalizedName(n, "_Pragma(map<string, TObjArray* >*)");
```
author_association: COLLABORATOR

Diff:
diff --git a/core/clingutils/src/TClingUtils.cxx b/core/clingutils/src/TClingUtils.cxx
index 0b1ad3499ae0d..42e6bbb424e67 100644
--- a/core/clingutils/src/TClingUtils.cxx
+++ b/core/clingutils/src/TClingUtils.cxx
@@ -4184,11 +4184,19 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   // Remove the _Atomic type specifyier if present before normalising
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(
+      normalizedNameStep1, TClassEdit::AtomicTypeNameHandlerRAII::EBehavior::kDetectStrip);
+
    // Still remove the std:: and default template argument for STL container and
    // normalize the location and amount of white spaces.
    TClassEdit::TSplitType splitname(normalizedNameStep1.c_str(),(TClassEdit::EModType)(TClassEdit::kLong64 | TClassEdit::kDropStd | TClassEdit::kDropStlDefault | TClassEdit::kKeepOuterConst));
    splitname.ShortType(norm_name,TClassEdit::kDropStd | TClassEdit::kDropStlDefault );
 
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(
+      norm_name, atomicTypeNameHandler_step1.IsAtomic() ? TClassEdit::AtomicTypeNameHandlerRAII::EBehavior::kReadd
+                                                        : TClassEdit::AtomicTypeNameHandlerRAII::EBehavior::kNoOp);
+
    // The result of this routine is by definition a fully qualified name.  There is an implicit starting '::' at the beginning of the name.
    // Depending on how the user typed their code, in particular typedef declarations, we may end up with an explicit '::' being
    // part of the result string.  For consistency, we must remove it.
diff --git a/core/foundation/inc/TClassEdit.h b/core/foundation/inc/TClassEdit.h
index d8599c5a97854..64dacdd1284f6 100644
--- a/core/foundation/inc/TClassEdit.h
+++ b/core/foundation/inc/TClassEdit.h
@@ -16,6 +16,7 @@
 #include <ROOT/RConfig.hxx>
 #include "RConfigure.h"
 #include <stdlib.h>
+#include <stdexcept>
 #ifdef R__WIN32
 #ifndef UNDNAME_COMPLETE
 #define UNDNAME_COMPLETE 0x0000
@@ -153,6 +154,51 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+   public:
+      enum class EBehavior : short {
+         kDetectStripReadd, // Detect if the _Atomic specifier is used, if yes, strip it and re-add it in the dtor
+         kDetectStrip,      // Detect if the _Atomic specifier is used, if yes, strip it
+         kReadd,            // Re-add the _Atomic specifier in the dtor
+         kNoOp              // Do nothing
+      };
+
+   private:
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      EBehavior fBehavior;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName, EBehavior behavior = EBehavior::kDetectStripReadd)
+         : fTypeName(typeName), fBehavior(behavior)
+      {
+         if (fBehavior == EBehavior::kReadd || fBehavior == EBehavior::kNoOp)
+            return;
+         if (0 == fTypeName.find(fgPrefix)) {
+            fIsAtomic = true;
+            if (fBehavior == EBehavior::kDetectStrip || fBehavior == EBehavior::kDetectStripReadd) {
+               fTypeName.erase(0, 8);
+               if (0 == fTypeName.size() || ')' != fTypeName[fTypeName.size() - 1]) {
+                  throw std::runtime_error("Cannot remove substring \")\" at the end of typename \"" + typeName + "\"");
+               }
+               fTypeName.pop_back();
+            }
+         }
+      }
+
+      bool IsAtomic() { return fIsAtomic; }
+
+      ~AtomicTypeNameHandlerRAII()
+      {
+         if (fBehavior == EBehavior::kReadd || (fBehavior == EBehavior::kDetectStripReadd && fIsAtomic)) {
+            fTypeName = fgPrefix + fTypeName + ")";
+         }
+      }
+   };
+
    void        Init(TClassEdit::TInterpreterLookupHelper *helper);
 
    std::string CleanType (const char *typeDesc,int mode = 0,const char **tail = nullptr);
diff --git a/core/foundation/src/TClassEdit.cxx b/core/foundation/src/TClassEdit.cxx
index 33fe1ec790136..df6f032e9f1da 100644
--- a/core/foundation/src/TClassEdit.cxx
+++ b/core/foundation/src/TClassEdit.cxx
@@ -875,6 +875,8 @@ void TClassEdit::GetNormalizedName(std::string &norm_name, std::string_view name
       return;
    }
 
+   AtomicTypeNameHandlerRAII nameHandler(norm_name);
+
    // Remove the std:: and default template argument and insert the Long64_t and change basic_string to string.
    TClassEdit::TSplitType splitname(norm_name.c_str(),(TClassEdit::EModType)(TClassEdit::kLong64 | TClassEdit::kDropStd | TClassEdit::kDropStlDefault | TClassEdit::kKeepOuterConst));
    splitname.ShortType(norm_name, TClassEdit::kDropStd | TClassEdit::kDropStlDefault | TClassEdit::kResolveTypedef | TClassEdit::kKeepOuterConst);
diff --git a/core/foundation/test/testClassEdit.cxx b/core/foundation/test/testClassEdit.cxx
index 7ca9678d70459..cef34b26e84c8 100644
--- a/core/foundation/test/testClassEdit.cxx
+++ b/core/foundation/test/testClassEdit.cxx
@@ -285,10 +285,20 @@ TEST(TClassEdit, DefAlloc)
    EXPECT_TRUE(TClassEdit::IsDefAlloc("class std::allocator<float>", "float"));
 }
 
-// https://github.com/root-project/root/issues/6607
+
 TEST(TClassEdit, GetNormalizedName)
 {
    std::string n;
+   
+   // https://github.com/root-project/root/issues/6607
    TClassEdit::GetNormalizedName(n, "std::vector<float, class std::allocator<float>>");
    EXPECT_STREQ("vector<float>", n.c_str());
+
+   // https://github.com/root-project/root/issues/18643
+   n.clear();
+   TClassEdit::GetNormalizedName(n, "_Atomic(map<string, TObjArray* >*)");
+   EXPECT_STREQ("_Atomic(map<string,TObjArray*>*)", n.c_str());
+
+   n.clear();
+   EXPECT_THROW(TClassEdit::GetNormalizedName(n, "_Atomic(map<string, TObjArray* >*"), std::runtime_error);
 }
diff --git a/core/meta/test/testTClass.cxx b/core/meta/test/testTClass.cxx
index c38b0fccca0bc..0ec52e57689ff 100644
--- a/core/meta/test/testTClass.cxx
+++ b/core/meta/test/testTClass.cxx
@@ -62,4 +62,10 @@ TEST(TClass, TypeNameDouble)
    // <double> should not be overwritten by <Double32_t>.
    TClass *clTypeID32 = TClass::GetClass(typeid(ROOT::Math::LorentzVector<ROOT::Math::PtEtaPhiE4D<double> >));
    EXPECT_EQ(clLVd, clTypeID32) << "LV<double> should have priority; typeid lookup should find it.";
-}
\ No newline at end of file
+}
+
+// https://github.com/root-project/root/issues/18643
+TEST(TClass, BuildRealData)
+{
+   TClass::GetClass("TClass")->BuildRealData();
+}


Reviews:
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T12:49:29Z
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T12:51:40Z
user: login: dpiparo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T12:56:46Z
user: login: dpiparo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T18:21:04Z
user: login: dpiparo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T18:21:28Z
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T19:47:47Z
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T19:50:51Z
user: login: dpiparo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T07:31:01Z
user: login: dpiparo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T07:33:27Z
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T15:13:13Z
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T15:15:42Z
user: login: dpiparo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T18:24:31Z
user: login: dpiparo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T18:27:11Z
user: login: dpiparo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T14:48:26Z
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:48:26Z
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:54:38Z
user: login: pcanal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T21:51:37Z
user: login: pcanal
body: LGTM.  Thanks.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T21:52:39Z

Review comments:
diff_hunk: @@ -153,6 +153,30 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A simple RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName) : fTypeName(typeName)
+      {
+         if (0 == fTypeName.find(fgPrefix)) {
path: core/foundation/inc/TClassEdit.h
user: login: pcanal
body: We should check and/or assert that `typeName.back() == ')'`
created_at: 2025-05-09T12:49:29Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,8 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler(normalizedNameStep1);
path: core/clingutils/src/TClingUtils.cxx
user: login: pcanal
body: This may or may not be doing what is intended.  `normalizedNameStep1` is used only as input to the next step.  The output is `norm_name` which after this addition will **not** contain the `_Atomic(...)` part.
created_at: 2025-05-09T12:51:40Z
author_association: MEMBER
diff_hunk: @@ -153,6 +153,30 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A simple RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName) : fTypeName(typeName)
+      {
+         if (0 == fTypeName.find(fgPrefix)) {
path: core/foundation/inc/TClassEdit.h
user: login: dpiparo
body: excellent idea.
created_at: 2025-05-09T12:56:46Z
author_association: MEMBER
diff_hunk: @@ -153,6 +153,30 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A simple RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName) : fTypeName(typeName)
+      {
+         if (0 == fTypeName.find(fgPrefix)) {
path: core/foundation/inc/TClassEdit.h
user: login: dpiparo
body: Done.
created_at: 2025-05-09T18:21:04Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,8 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler(normalizedNameStep1);
path: core/clingutils/src/TClingUtils.cxx
user: login: dpiparo
body: Addressed.

created_at: 2025-05-09T18:21:28Z
author_association: MEMBER
diff_hunk: @@ -153,6 +154,33 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A simple RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName) : fTypeName(typeName)
+      {
+         if (0 == fTypeName.find(fgPrefix)) {
+            fIsAtomic = true;
+            fTypeName.erase(0, 8);
+            if (0 == fTypeName.size() || ')' != fTypeName[fTypeName.size() - 1]) {
+               throw std::runtime_error("Cannot remove substring \")\" at the end of typename \""+typeName+"\"");
path: core/foundation/inc/TClassEdit.h
user: login: pcanal
body: Note that the calling code is very unlikely to be exception safe.
created_at: 2025-05-09T19:47:47Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,10 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   norm_name = normalizedNameStep1;
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(normalizedNameStep1);
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(norm_name);
path: core/clingutils/src/TClingUtils.cxx
user: login: pcanal
body: The first line is a fully copy of the (possibly long type).  We might want to extend AtomicTypeNameHandlerRAII so that we can write 'just':
```suggestion
   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(normalizedNameStep1);
   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(norm_name, atomicTypeNameHandler_step1.IsAtomic());
```
where the second one just record the reference and trust the requested state.
created_at: 2025-05-09T19:50:51Z
author_association: MEMBER
diff_hunk: @@ -153,6 +154,33 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A simple RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName) : fTypeName(typeName)
+      {
+         if (0 == fTypeName.find(fgPrefix)) {
+            fIsAtomic = true;
+            fTypeName.erase(0, 8);
+            if (0 == fTypeName.size() || ')' != fTypeName[fTypeName.size() - 1]) {
+               throw std::runtime_error("Cannot remove substring \")\" at the end of typename \""+typeName+"\"");
path: core/foundation/inc/TClassEdit.h
user: login: dpiparo
body: Yes. The exception is expressive enough to explain why ROOT probably crashed though perhaps?
created_at: 2025-05-10T07:31:01Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,10 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   norm_name = normalizedNameStep1;
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(normalizedNameStep1);
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(norm_name);
path: core/clingutils/src/TClingUtils.cxx
user: login: dpiparo
body: Thanks. I do not fully understand why you mention a "copy". The RAAI stores a reference to the string (see https://github.com/root-project/root/pull/18674/files#diff-bc1b5fbce8a4116a51b5d348e8510df04cc97eac8a74e75889bf86e561c59212R160 ) but maybe you are referring to something else - sorry to be thick.
created_at: 2025-05-10T07:33:27Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,10 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   norm_name = normalizedNameStep1;
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(normalizedNameStep1);
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(norm_name);
path: core/clingutils/src/TClingUtils.cxx
user: login: pcanal
body: line 4187 makes a copy of the intermediary string.
created_at: 2025-05-12T15:13:13Z
author_association: MEMBER
diff_hunk: @@ -153,6 +154,33 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A simple RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName) : fTypeName(typeName)
+      {
+         if (0 == fTypeName.find(fgPrefix)) {
+            fIsAtomic = true;
+            fTypeName.erase(0, 8);
+            if (0 == fTypeName.size() || ')' != fTypeName[fTypeName.size() - 1]) {
+               throw std::runtime_error("Cannot remove substring \")\" at the end of typename \""+typeName+"\"");
path: core/foundation/inc/TClassEdit.h
user: login: pcanal
body: It is probably fine in practice but still iffy.  i.e. it is unknown what state catching this exception would leave ROOT in; we should assume 'unusable' and if we do, then this might as well be a call to `Fatal`.
created_at: 2025-05-12T15:15:42Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,10 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   norm_name = normalizedNameStep1;
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(normalizedNameStep1);
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(norm_name);
path: core/clingutils/src/TClingUtils.cxx
user: login: dpiparo
body: but isn't the suggested change requiring a copy anyway?
created_at: 2025-05-12T18:24:31Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,10 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   norm_name = normalizedNameStep1;
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(normalizedNameStep1);
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(norm_name);
path: core/clingutils/src/TClingUtils.cxx
user: login: dpiparo
body: ah maybe you mean checking if norm_name uses "_Atomic" as a type specifier, and if yes go through the expensive path and if not do as we do now, right?
created_at: 2025-05-12T18:27:11Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,10 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   norm_name = normalizedNameStep1;
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(normalizedNameStep1);
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(norm_name);
path: core/clingutils/src/TClingUtils.cxx
user: login: dpiparo
body: implemented.
created_at: 2025-05-13T14:48:26Z
author_association: MEMBER
diff_hunk: @@ -153,6 +154,36 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A simple RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName, bool isAtomic = false) : fTypeName(typeName)
+      {
+         if (isAtomic) {
+            fIsAtomic = isAtomic;
+         } else if (0 == fTypeName.find(fgPrefix)) {
+            fIsAtomic = true;
+            fTypeName.erase(0, 8);
+            if (0 == fTypeName.size() || ')' != fTypeName[fTypeName.size() - 1]) {
+               throw std::runtime_error("Cannot remove substring \")\" at the end of typename \"" + typeName + "\"");
+            }
+            fTypeName.pop_back();
+         }
+      }
path: core/foundation/inc/TClassEdit.h
user: login: pcanal
body: ```suggestion
      AtomicTypeNameHandlerRAII(std::string &typeName, bool isAtomic) : fTypeName(typeName), fIsAtomic(isAtomic) {}

      AtomicTypeNameHandlerRAII(std::string &typeName) : fTypeName(typeName)
      {
         if (0 == fTypeName.find(fgPrefix)) {
            fIsAtomic = true;
            fTypeName.erase(0, 8);
            if (0 == fTypeName.size() || ')' != fTypeName[fTypeName.size() - 1]) {
               throw std::runtime_error("Cannot remove substring \")\" at the end of typename \"" + typeName + "\"");
            }
            fTypeName.pop_back();
         }
      }
```
seems clearer.
created_at: 2025-05-13T15:48:26Z
author_association: MEMBER
diff_hunk: @@ -4184,6 +4184,10 @@ void ROOT::TMetaUtils::GetNormalizedName(std::string &norm_name, const clang::Qu
    cling::Interpreter::PushTransactionRAII clingRAII(const_cast<cling::Interpreter*>(&interpreter));
    normalizedType.getAsStringInternal(normalizedNameStep1,policy);
 
+   norm_name = normalizedNameStep1;
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_step1(normalizedNameStep1);
+   TClassEdit::AtomicTypeNameHandlerRAII atomicTypeNameHandler_norm_name(norm_name);
path: core/clingutils/src/TClingUtils.cxx
user: login: pcanal
body: > ah maybe you mean checking if norm_name uses "_Atomic" as a type specifier, and if yes go through the expensive path and if not do as we do now, right?

Sort of.  I meant if `normalizedNameStep1` start with "_Atomic" then strip it from `normalizedNameStep1` and add it back to both `normalizedNameStep1` and `norm_name`.

`TClassEdit::TSplitType` can not (as far I remember) add the `_Atomic`, at least on the outer edge of the name.  I.e the expected input if of the form `namespace_scope::name_of_template< TemplateArgs... >` and will not change `name_of_template`.

And re-reading what I just described, my propose is still sub-optimal.  What we really should be done is:

 if `normalizedNameStep1` start with "_Atomic" then strip it from `normalizedNameStep1` and add it back to only `norm_name`.

(because `normalizedNameStep1` is not use after being used an input of `TClassEdit::TSplitType` 
created_at: 2025-05-13T15:54:38Z
author_association: MEMBER
diff_hunk: @@ -153,6 +154,50 @@ namespace TClassEdit {
       TSplitType &operator=(const TSplitType &) = delete;
    };
 
+   /// A RAII helper to remove and readd enclosing _Atomic()
+   /// It expects no spaces at the beginning or end of the string
+   class AtomicTypeNameHandlerRAII {
+   public:
+      enum class EBehavior : short {
+         kDetectStripReadd, // Detect if the _Atomic specifier is used, if yes, strip it and re-add it in the dtor
+         kDetectStrip,      // Detect if the _Atomic specifier is used, if yes, strip it
+         kReadd,            // Re-add the _Atomic specifier in the dtor
+         kNoOp              // Do nothing
+      };
+
+   private:
+      std::string &fTypeName;
+      bool fIsAtomic = false;
+      EBehavior fBehavior;
+      const char *fgPrefix = "_Atomic(";
+
+   public:
+      AtomicTypeNameHandlerRAII(std::string &typeName, EBehavior behavior = EBehavior::kDetectStripReadd)
+         : fTypeName(typeName), fBehavior(behavior)
+      {
+         if (fBehavior == EBehavior::kReadd || fBehavior == EBehavior::kNoOp) return;
path: core/foundation/inc/TClassEdit.h
user: login: pcanal
body: ```suggestion
         if (fBehavior == EBehavior::kReadd || fBehavior == EBehavior::kNoOp)
            return;
```
created_at: 2025-05-13T21:51:36Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-09T12:19:26Z
body: ## Test Results
     5 files       5 suites   17h 4m 0s ⏱️
 2 724 tests  2 723 ✅ 1 💤 0 ❌
13 198 runs  13 198 ✅ 0 💤 0 ❌

Results for commit a39410c5.

[test-results]:data:application/gzip;base64,H4sIAH+8I2gC/1WPzQ6DIBAGX8Vw7kH+RPsyBncxIVVpEE5N371rVWqPM192kn2x0U9uZfdK3yq2Zp8KYI42+bAQNlypmhSNaZuFEerEfs0Au5M/9/BPcryI0fqJRIn0LsYQDxPzslW55F174Fn9d3v1PLlGv3xtQphnnwiYlZ3iNWgzGi3RDjiglU4jWhQAQmILDT3o2PsDEwIvNA0BAAA=

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: dpiparo
created_at: 2025-05-13T20:22:30Z
body: Thanks for the suggestion. Now it's part of the changes.
author_association: MEMBER