This is an archive of the discontinued LLVM Phabricator instance.

[libc++] Remove UB in list, forward_list and __hash_table
ClosedPublic

Authored by ldionne on Apr 23 2021, 4:22 PM.

Details

Summary

This patch removes undefined behavior in list and forward_list and __hash_table
caused by improperly beginning and ending the lifetime of the various node
classes. It allows removing the _LIBCPP_STANDALONE_DEBUG macro from
these node types since we now properly begin and end their lifetime,
meaning that we won't trip up constructor homing.

See https://reviews.llvm.org/D98750 for more information on what prompted
this patch.

Diff Detail

Event Timeline

akhuang requested review of this revision.Apr 23 2021, 4:22 PM
akhuang created this revision.
Herald added a project: Restricted Project. · View Herald TranscriptApr 23 2021, 4:22 PM
Herald added a reviewer: Restricted Project. · View Herald Transcript
akhuang updated this revision to Diff 340196.Apr 23 2021, 4:26 PM

remove commented out code

akhuang updated this revision to Diff 340198.Apr 23 2021, 4:28 PM

remove commented code

This looks somewhat similar to D99624. Does it attempt to address the same issue? @ldionne you might want to have a look at this.

Quuxplusone added a subscriber: Quuxplusone.

@Mordante: yes it does. :)
@akhuang: My understanding is that removing-the-UB is needed (or at least desired) for the benefit of some particular sanitizer; could you add info to the review summary (and commit message) about that?
Also, are we really interested in getting that benefit for C++03 code? Arguably it would be better to preserve the existing behavior for C++03 and fix the behavior only for C++>=11. Is it physically possible to go that route? and if so, does that eliminate a lot of the ifdefs in this PR? (I'm thinking we could just have one ifdef around the union itself, and then the rest of the code wouldn't need ifdefs at all.) The behavior would remain UB, but unchanged, in C++03; and we could say "if you want to use such-and-such sanitizer, please compile as C++>=11." That won't be a hardship for the user, will it?

@Mordante: yes it does. :)
@akhuang: My understanding is that removing-the-UB is needed (or at least desired) for the benefit of some particular sanitizer; could you add info to the review summary (and commit message) about that?
Also, are we really interested in getting that benefit for C++03 code? Arguably it would be better to preserve the existing behavior for C++03 and fix the behavior only for C++>=11. Is it physically possible to go that route? and if so, does that eliminate a lot of the ifdefs in this PR? (I'm thinking we could just have one ifdef around the union itself, and then the rest of the code wouldn't need ifdefs at all.) The behavior would remain UB, but unchanged, in C++03; and we could say "if you want to use such-and-such sanitizer, please compile as C++>=11." That won't be a hardship for the user, will it?

So the motivation for this is for a debug info optimization that homes debug info for classes to their constructors, and it relies on constructors being called (and we'd like to enable that mode by default in llvm). We don't necessarily care whether there's UB or not, but in this situation we wouldn't have debug info for these types.

I think making it only work for only C++>=11 could be fine, though.

For the debug info issue, we can also just add an attribute to these types (https://reviews.llvm.org/D98750); I think @ldionne preferred solving this by fixing the UB directly though.

akhuang updated this revision to Diff 340581.Apr 26 2021, 10:33 AM

Remove fix for c++03.

LGTM at this point. So I guess the next question (besides how it looks to @ldionne!) is, should you do it for forward_list etc. in this same PR? Do you know already the complete list of types that need this kind of patch done for them? If so, I would think it's appropriate to do them all together in this PR.

libcxx/include/list
354–357

Any reason not to =default this constructor? (And I'd make it explicit, since there's no reason not to, and it might ease overload resolution just a tiny bit. In my codebases, the explicit keyword basically means "Look out, here comes a constructor!")

libcxx/include/list
332

Nit: Indent 4 spaces to match the indentation everywhere else in this file. (I blame clang-format. ;))

akhuang updated this revision to Diff 341014.Apr 27 2021, 3:58 PM

Apply union fix to forward_list_node and hash_node

Seems like it's a little less straightforward for some of the other types (__tree_node, __value_type and __hash_value_type), especially since they explicitly delete the constructors/destructors and I don't have a great understanding of why. Should probably get @EricWF's opinion on this?

I'm still somewhat leaning toward just adding an attribute to those types to solve the debug info issue (https://reviews.llvm.org/D98750)

libcxx/include/list
354–357

apparently using =default complains because __value_ doesn't have a default constructor

Has this been tested with various sanitizers on? Does it give warnings about weird object lifetimes?

libcxx/include/list
332

I'm a bit worried that there are combinations of types and compiler flags that will align a subobject differently when it's in a union than when it's not.
I need to investigate further.

1083

Maybe we can sink these repeated lines of code into a function?

Seems like it's a little less straightforward for some of the other types (__tree_node, __value_type and __hash_value_type), especially since they explicitly delete the constructors/destructors and I don't have a great understanding of why. Should probably get @EricWF's opinion on this?

I'm still somewhat leaning toward just adding an attribute to those types to solve the debug info issue (https://reviews.llvm.org/D98750)

I agree that the attribute is a non-complicated and good way to address those types.

Has this been tested with various sanitizers on? Does it give warnings about weird object lifetimes?

Tried running tests with sanitizers (Address, Memory, Undefined) - doesn't look like there are any new failures.

akhuang updated this revision to Diff 341749.Apr 29 2021, 7:41 PM

Move some of the #ifndefs in forward_list into a function

akhuang updated this revision to Diff 342788.May 4 2021, 10:08 AM

Fix #ifdef for function

ldionne commandeered this revision.Sep 6 2023, 11:16 AM
ldionne edited reviewers, added: akhuang; removed: ldionne.

[Github PR transition cleanup]

Commandeering to poke CI, rebase, etc.

Herald added a project: Restricted Project. · View Herald TranscriptSep 6 2023, 11:16 AM
ldionne updated this revision to Diff 556093.Sep 6 2023, 4:34 PM
ldionne retitled this revision from [libc++] Remove UB in std::list to [libc++] Rework node creation and destruction in std::list and std::forward_list.
ldionne edited the summary of this revision. (Show Details)

Update. This is still a WIP. This rebases the patch and reworks how it's achieved to

  1. Increase consistency
  2. Remove code duplication
  3. Simplify the code

The resulting patch touches more lines, but it becomes a lot easier to verify the correctness of the resulting code.

This is still a WIP, I need to update std::list but this is enough for today.

I am opting to do a refactoring in https://github.com/llvm/llvm-project/pull/65614 first.

ldionne updated this revision to Diff 556153.Sep 7 2023, 8:02 AM
ldionne retitled this revision from [libc++] Rework node creation and destruction in std::list and std::forward_list to [libc++] Remove UB in list, forward_list and __hash_table.
ldionne edited the summary of this revision. (Show Details)

Rebase on top of refactoring in https://github.com/llvm/llvm-project/pull/65614

This also allows removing the _LIBCPP_STANDALONE_DEBUG attribute from a few types since constructor homing should now work for them. For more details, see https://reviews.llvm.org/D98750

Awesome! I'm really glad to see this & the UB being cleaned up.

(though this does mean the debug info will be broken for C++03 mode, yeah? I don't think we/Google care about that, but other people might... )

(though this does mean the debug info will be broken for C++03 mode, yeah? I don't think we/Google care about that, but other people might... )

I could keep the _LIBCPP_STANDALONE_DEBUG attribute on the classes for the C++03 case, I guess!

ldionne updated this revision to Diff 556950.Sep 18 2023, 7:42 AM

Rebase onto main, which contains the refactoring. Keep _LIBCPP_STANDALONE_DEBUG on the node types to avoid breaking debug info in C++03.

I don't know how to add a test for this change, suggestions welcome.

Rebase onto main, which contains the refactoring. Keep _LIBCPP_STANDALONE_DEBUG on the node types to avoid breaking debug info in C++03.

I don't know how to add a test for this change, suggestions welcome.

I don't think the original commit had test coverage for this, probably - so "just as good" to still not have it.

I think the way to test it, if you wanted to, would be to llvm-dwarfdump a simple usage and check that the type definition is available.

It'd look something like...

RUN: clang x.cpp -g -c -o %t.o
RUN: llvm-dwarfdump %t.o | FileCheck %s

CHECK:   DW_AT_name "SomeType"
CHECK-NOT: DW_AT_declaration
CHECK-NOT: DW_TAG
CHECK:  DW_AT_byte_size
CHECK: DW_TAG

(This is sort of hedging my bets a bit - checking both that it's not a declaration, and that it does have a size (size isn't included in declarations) - there's some risk that the attributes are out of order and that declaration or size could come before name, but I don't think LLVM produces type attributes in that order - pretty sure the name comes first)

RUN: clang x.cpp -g -c -o %t.o
RUN: llvm-dwarfdump %t.o | FileCheck %s

CHECK:   DW_AT_name "SomeType"
CHECK-NOT: DW_AT_declaration
CHECK-NOT: DW_TAG
CHECK:  DW_AT_byte_size
CHECK: DW_TAG

(This is sort of hedging my bets a bit - checking both that it's not a declaration, and that it does have a size (size isn't included in declarations) - there's some risk that the attributes are out of order and that declaration or size could come before name, but I don't think LLVM produces type attributes in that order - pretty sure the name comes first)

Thanks for the suggestion! We don't have access to FileCheck in the libc++ tests unfortunately, so this would require a bunch of additional work (as outlined in https://github.com/llvm/llvm-project/pull/65917). I also tried running this locally and I don't see anything that looks like DW_AT_name "SomeType" (where I imagine SomeType should be e.g. __list_node in some form)?

RUN: clang x.cpp -g -c -o %t.o
RUN: llvm-dwarfdump %t.o | FileCheck %s

CHECK:   DW_AT_name "SomeType"
CHECK-NOT: DW_AT_declaration
CHECK-NOT: DW_TAG
CHECK:  DW_AT_byte_size
CHECK: DW_TAG

(This is sort of hedging my bets a bit - checking both that it's not a declaration, and that it does have a size (size isn't included in declarations) - there's some risk that the attributes are out of order and that declaration or size could come before name, but I don't think LLVM produces type attributes in that order - pretty sure the name comes first)

Thanks for the suggestion! We don't have access to FileCheck in the libc++ tests unfortunately, so this would require a bunch of additional work (as outlined in https://github.com/llvm/llvm-project/pull/65917). I also tried running this locally and I don't see anything that looks like DW_AT_name "SomeType" (where I imagine SomeType should be e.g. __list_node in some form)?

looks like it's __forward_list_node in this case? Here's an example with/without the attribute at some version of libc++ I have installed locally:

$ clang++-tot test.cpp -g -c -stdlib=libc++ && llvm-dwarfdump-tot test.o | grep "DW_AT_name.*\"__forward_list_node<" -A5
                    DW_AT_name  ("__forward_list_node<int, void *>")
                    DW_AT_byte_size     (0x10)
                    DW_AT_decl_file     ("/usr/local/google/home/blaikie/dev/llvm/build/default/bin/../include/c++/v1/forward_list")
                    DW_AT_decl_line     (324)
...
$ clang++-tot test.cpp -g -c -stdlib=libc++ && llvm-dwarfdump-tot test.o | grep "DW_AT_name.*\"__forward_list_node<" -A5
                    DW_AT_name  ("__forward_list_node<int, void *>")
                    DW_AT_declaration   (true)
...
RUN: clang x.cpp -g -c -o %t.o
RUN: llvm-dwarfdump %t.o | FileCheck %s

CHECK:   DW_AT_name "SomeType"
CHECK-NOT: DW_AT_declaration
CHECK-NOT: DW_TAG
CHECK:  DW_AT_byte_size
CHECK: DW_TAG

(This is sort of hedging my bets a bit - checking both that it's not a declaration, and that it does have a size (size isn't included in declarations) - there's some risk that the attributes are out of order and that declaration or size could come before name, but I don't think LLVM produces type attributes in that order - pretty sure the name comes first)

Thanks for the suggestion! We don't have access to FileCheck in the libc++ tests unfortunately, so this would require a bunch of additional work (as outlined in https://github.com/llvm/llvm-project/pull/65917). I also tried running this locally and I don't see anything that looks like DW_AT_name "SomeType" (where I imagine SomeType should be e.g. __list_node in some form)?

We do have access to filecheck sometimes. We could just check for the binary, and make our decision based off that. Though, that means we'll likely not have the test running on the bots (right away), but we can still write the test.
I don't know that it'll provide a ton of value though.


On another note, I think this patch could be a lot smaller and introduce fewer #ifdef branches. I think we should simply default-construct the node, then initialize each member as we were before. We can also skip the calls to the destructors because they have no observable effects.
This has the benefit of reducing the amount of code duplication between the branches, as well as keeping the pre/post conditions of the node type after construction the same. You don't need to add additional constructors.

Let me know if I've missed something.

libcxx/include/__hash_table
1398

I don't know that we need to call the destructor here.
Do we?

1398

Lets drop the calls to the destructor. They should have no affect, and so it's more readable without them as it makes clear they don't perform any user-observable behaviors.

2237

Why are we initializing more in this branch than we are in the other?

I would rather keep the two sets of behavior as convergent as possible.

2237

In fact, why not simply make the constructor an nop, then keep using the code below to initialize the members.

It's easier to reason about one conditional compilation block than it is two.

2237

Same note as above. Simply default construct the object, then initialize it the same way we were.

libcxx/include/forward_list
331

Is there a correctness reason for using the union over some other form of aligned storage?

332

I don't like macros that get defined mid-file like this. We typically don't do it, and I would like to avoid it.

620

Same comment here.

libcxx/include/list
356

Do we cause ourselves any problems not starting the lifetime of the object in C++03 mode now that it's got a user defined destructor?

I suspect not, because it's not like the type was always trivially destructible, but maybe?

ldionne marked 13 inline comments as done.Sep 28 2023, 10:49 AM
ldionne added inline comments.
libcxx/include/__hash_table
1398

As discussed during live review, we're going to call the destructor for symmetry with calling construct_at.

2237

As discussed during live review, the new approach will initialize the node in all standard modes.

libcxx/include/forward_list
331

As discussed, I think the main benefit is readability and probably the debugging experience in non-C++03 mode.

libcxx/include/list
356

This will become moot with the new approach we discussed.

ldionne updated this revision to Diff 557451.Sep 28 2023, 11:13 AM
ldionne marked 4 inline comments as done.
ldionne edited the summary of this revision. (Show Details)

Update after discussion with Eric. This allows fixing the UB in all standard modes and simplifies the code quite a bit!

dblaikie added inline comments.Sep 29 2023, 10:29 AM
libcxx/include/forward_list
331

Readability seems a bit limited if the cost is #ifdefs and having the other implementation here too, though, yeah? Like this code would be easier, I think, to read and modify if there was only one implementation even if it's the aligned storage one, rather than having two. Less is easier to read, generally.

Debuggability - yeah, that's true, though probably not super important except for standard library authors? (& sounds like you're not convinced/certain it'd be especially helpful to you personally?) - if you want to inspect the value in the node directly, without invoking any code (eg: debugging a core dump) then the union is easier (you don't have to write out the cast in a debugger expression evaluator to get the value from the aligned storage)

(I'm not making some firm statement here - just my 2c, feel free to disregard)

Update after discussion with Eric. This allows fixing the UB in all standard modes and simplifies the code quite a bit!

Oh, also meant to say: This is a great improvement!

ldionne updated this revision to Diff 557572.Oct 3 2023, 11:16 AM

Fix CI issues.

ldionne added inline comments.Oct 3 2023, 11:17 AM
libcxx/include/forward_list
331

I don't have a strong opinion, but I think the "inspecting a value from the debugger is easier with a union" was the main motivating factor for doing this.

dblaikie added inline comments.Oct 3 2023, 11:37 AM
libcxx/include/forward_list
331

Fair - yeah, if I were maintaining only one version and I could write the union one, I would. But if it comes to maintaining two versions - I probably wouldn't bother with two versions for that benefit. *shrug*

ldionne updated this revision to Diff 557601.Oct 4 2023, 2:25 PM

Fix gnu_cxx test on GCC. Remove CFI tests which are unnecessary according to EricWF (after offline discussion).

ldionne accepted this revision.Oct 5 2023, 6:13 AM
This revision is now accepted and ready to land.Oct 5 2023, 6:13 AM
EricWF accepted this revision.Oct 5 2023, 9:49 AM
This revision was automatically updated to reflect the committed changes.
Michael137 added a subscriber: Michael137.EditedOct 6 2023, 1:33 AM

Looks like this broke the LLDB buildbot on Darwin: https://green.lab.llvm.org/green/view/LLDB/job/lldb-cmake/61029/console

Specifically the formatters:

Failed Tests (2):
  lldb-api :: commands/expression/import-std-module/queue/TestQueueFromStdModule.py
  lldb-api :: functionalities/data-formatter/data-formatter-stl/generic/unordered/TestDataFormatterGenericUnordered.py
AssertionError: Ran command:
"frame variable map"

Got output:
(std::unordered_map<int, std::string>) map = size=5 {}

Expecting regex pattern: "std::unordered_map" (was found, matched "std::unordered_map")
Expecting regex pattern: "\[0\] = \{\s*first = " (was not found)

Presumably because we removed the _LIBCPP_STANDALONE_DEBUG. I don't think we do constructor homing on Darwin since we do -fstandalone-debug

Temporarily rolled back due to the LLDB failures.

Looks like this broke the LLDB buildbot on Darwin: https://green.lab.llvm.org/green/view/LLDB/job/lldb-cmake/61029/console

Specifically the formatters:

Failed Tests (2):
  lldb-api :: commands/expression/import-std-module/queue/TestQueueFromStdModule.py
  lldb-api :: functionalities/data-formatter/data-formatter-stl/generic/unordered/TestDataFormatterGenericUnordered.py
AssertionError: Ran command:
"frame variable map"

Got output:
(std::unordered_map<int, std::string>) map = size=5 {}

Expecting regex pattern: "std::unordered_map" (was found, matched "std::unordered_map")
Expecting regex pattern: "\[0\] = \{\s*first = " (was not found)

Presumably because we removed the _LIBCPP_STANDALONE_DEBUG. I don't think we do constructor homing on Darwin since we do -fstandalone-debug

Adding/removing the attribute shouldn't make any difference on Darwin. (the standalone debug attribute says "treat this type as though it were compiled with -fstandalone-debug" and Darwin's default is -fstandalone-debug - so adding/removing the attribute shouldn't change the behavior - it's possible there are bugs here, but I'd wager that's not the case/is a red herring)

Looks like this broke the LLDB buildbot on Darwin: https://green.lab.llvm.org/green/view/LLDB/job/lldb-cmake/61029/console

Specifically the formatters:

Failed Tests (2):
  lldb-api :: commands/expression/import-std-module/queue/TestQueueFromStdModule.py
  lldb-api :: functionalities/data-formatter/data-formatter-stl/generic/unordered/TestDataFormatterGenericUnordered.py
AssertionError: Ran command:
"frame variable map"

Got output:
(std::unordered_map<int, std::string>) map = size=5 {}

Expecting regex pattern: "std::unordered_map" (was found, matched "std::unordered_map")
Expecting regex pattern: "\[0\] = \{\s*first = " (was not found)

Presumably because we removed the _LIBCPP_STANDALONE_DEBUG. I don't think we do constructor homing on Darwin since we do -fstandalone-debug

Adding/removing the attribute shouldn't make any difference on Darwin. (the standalone debug attribute says "treat this type as though it were compiled with -fstandalone-debug" and Darwin's default is -fstandalone-debug - so adding/removing the attribute shouldn't change the behavior - it's possible there are bugs here, but I'd wager that's not the case/is a red herring)

Good point, though I did confirm that this patch caused the breakage.
Reason I pointed it at the attribute was following error:

error: warning: warning: got name from symbols: C
error: Couldn't look up symbols:
  __ZNSt3__19__voidifyB7v180000INS_11__list_nodeI1CPvEEEES3_RT_

Which on first glance sounds like a "lack of debug info" problem. Though your point makes sense, it shouldn't affect Darwin since it's already the default. Will investigate what else it could be

Michael137 added a comment.EditedOct 8 2023, 8:47 AM

Looks like this broke the LLDB buildbot on Darwin: https://green.lab.llvm.org/green/view/LLDB/job/lldb-cmake/61029/console

Specifically the formatters:

Failed Tests (2):
  lldb-api :: commands/expression/import-std-module/queue/TestQueueFromStdModule.py
  lldb-api :: functionalities/data-formatter/data-formatter-stl/generic/unordered/TestDataFormatterGenericUnordered.py
AssertionError: Ran command:
"frame variable map"

Got output:
(std::unordered_map<int, std::string>) map = size=5 {}

Expecting regex pattern: "std::unordered_map" (was found, matched "std::unordered_map")
Expecting regex pattern: "\[0\] = \{\s*first = " (was not found)

Presumably because we removed the _LIBCPP_STANDALONE_DEBUG. I don't think we do constructor homing on Darwin since we do -fstandalone-debug

Adding/removing the attribute shouldn't make any difference on Darwin. (the standalone debug attribute says "treat this type as though it were compiled with -fstandalone-debug" and Darwin's default is -fstandalone-debug - so adding/removing the attribute shouldn't change the behavior - it's possible there are bugs here, but I'd wager that's not the case/is a red herring)

Good point, though I did confirm that this patch caused the breakage.
Reason I pointed it at the attribute was following error:

error: warning: warning: got name from symbols: C
error: Couldn't look up symbols:
  __ZNSt3__19__voidifyB7v180000INS_11__list_nodeI1CPvEEEES3_RT_

Which on first glance sounds like a "lack of debug info" problem. Though your point makes sense, it shouldn't affect Darwin since it's already the default. Will investigate what else it could be

Ah the problem is most likely because __value_ is now wrapped in an anonymous union. Which breaks the formatter's assumption:

// In LibCxxUnorderedMap.cpp
node_sp = m_next_element->Cast(m_node_type.GetPointerType()) 
        ->Dereference(error);                                            
if (!node_sp || error.Fail())                                               
    return nullptr;                                                         
                                                                            
value_sp = node_sp->GetChildMemberWithName("__value_");

Should be able to fix this fairly quick. The other test is failing for a different reason by the looks of it. But it is for a much less widely used feature and I don't think it really needs to block this from landing if we don't get to the bottom of it soon.

Side note, we should really improve the error reporting when these formatters fail (currently I don't see any). So we can get to the bottom of the failures more quickly.

Personally - maybe this added complexity (combined with the existing slight increase in complexity of having #ifdef'd code) is enough to tip in favor of having the one portable implementation - despite the possible slight reduction in implementation debuggability, since it doesn't sound like anyone's here saying they really need that feature?

ldionne reopened this revision.Oct 12 2023, 8:11 PM

Personally - maybe this added complexity (combined with the existing slight increase in complexity of having #ifdef'd code) is enough to tip in favor of having the one portable implementation - despite the possible slight reduction in implementation debuggability, since it doesn't sound like anyone's here saying they really need that feature?

I agree. However, after speaking with @Michael137 , it would actually make LLDB's life harder if I were to always use the C++03 variant because right now they only handle the union version of the code and the previous version of the code. @Michael137 tells me I can re-commit this now, so I'll do that.

This revision is now accepted and ready to land.Oct 12 2023, 8:11 PM

Actually I'll wait for tomorrow morning so the LLDB folks can follow-up with the fix quickly after.

Personally - maybe this added complexity (combined with the existing slight increase in complexity of having #ifdef'd code) is enough to tip in favor of having the one portable implementation - despite the possible slight reduction in implementation debuggability, since it doesn't sound like anyone's here saying they really need that feature?

I agree. However, after speaking with @Michael137 , it would actually make LLDB's life harder if I were to always use the C++03 variant because right now they only handle the union version of the code and the previous version of the code. @Michael137 tells me I can re-commit this now, so I'll do that.

Oh, it looks like it would handle the 03 variant today - because it'd find the __value_ member on the first lookup, then it only falls back to looking through the anonymous union if that fails? Or does that fail later on because the __value_ member isn't of the expected type in the 03 case (since it's just the raw buffer).

Pity to lose the ability to pretty print the 03 version :/

Personally - maybe this added complexity (combined with the existing slight increase in complexity of having #ifdef'd code) is enough to tip in favor of having the one portable implementation - despite the possible slight reduction in implementation debuggability, since it doesn't sound like anyone's here saying they really need that feature?

I agree. However, after speaking with @Michael137 , it would actually make LLDB's life harder if I were to always use the C++03 variant because right now they only handle the union version of the code and the previous version of the code. @Michael137 tells me I can re-commit this now, so I'll do that.

Oh, it looks like it would handle the 03 variant today - because it'd find the __value_ member on the first lookup, then it only falls back to looking through the anonymous union if that fails? Or does that fail later on because the __value_ member isn't of the expected type in the 03 case (since it's just the raw buffer).

Pity to lose the ability to pretty print the 03 version :/

We can certainly add support for the C++03 format too. I don't think have any test coverage for C++03 but wouldn't be too difficult to add. Will have a look at that next week