C++23 - final touches

I start a new thread about C++23 because the old one was archived: https://cplusplus.com/forum/lounge/283965/

The first post of the old thread claims C++23 is "feature-complete" but that doesn't mean things can still not change. It looks like the "national bodies" have now come with feedback and it will be interesting to see what changes it might lead to.

https://github.com/cplusplus/nbballot/issues?q=sort%3Aupdated-desc

I don't know how many of them will actually get fixed for C++23. Some of them could probably wait but it's still interesting to follow.
Last edited on
Here are two national body comments that I find interesting:

NB comments: https://github.com/cplusplus/nbballot/issues/407 https://github.com/cplusplus/nbballot/issues/471
Related paper: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2644r0.pdf
You might think that
1
2
3
4
for (int n : std::vector<std::vector<int>>{{1,2},{3,4}}[0])
{
	std::cout << n << "\n";
}
should print "1" and "2" but this is actually UB because the vector that we loop over gets destroyed before the first iteration starts. The NB comments want to fix this issue but it has been like this since C++11 so does it really need to get fixed before C++23?

NB comment: https://github.com/cplusplus/nbballot/issues/425
Related paper: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1774r8.pdf
Some people have been looking forward to be able to write assumptions using a standardized syntax in C++23.
For example:
1
2
3
4
5
int foo(int x)
{
	[[assume(x >= 0)]];
	return x / 2;
}
The compiler might be able to use the assumption to implement the division more efficiently but if the assumption is wrong it leads to UB. This feature has already been added to the C++23 draft but the NB comment argues that it "introduces more problems than it solves" but it seems more concerned with ABI issues rather than UB.
Last edited on
I class those as DR's.

The range-for lifetime issue is a real problem which IMO should have been sorted out before range-for was introduced - but hey we have this camel here we'd like to sell you...

Re the range-for example. VS2022 intellicode gives warning C26815 - "The pointer is dangling because it points at a temporary instance which was destroyed". And if you ignore this and run the code you get a run-time exception.

This is OK though:

1
2
for (auto n : std::vector<std::vector<int>> { {1,2},{3,4} } )
		std::cout << n[0] << "\n";


which causes many to shout for nurse!
seeplus wrote:
I class those as DR's.

Doesn't DR mean compilers are expected to apply the change to earlier versions of the standard?

Is it wise to treat the range-based for loop issue as a DR when it could potentially break code? I admit it's unlikely. It might even fix some bugs but I'm afraid it might lead to a confusing situation about what rules actually apply (in practice) when using older versions of the standard. I think it's much easier to say that you need C++23 (or C++26) than to say you need a compiler that implements DR XYZ.

I'm not sure if the comment about the assumptions actually needs to be addressed but if they did it now before C++23 it wouldn't be treated as a DR because C++23 has not been released yet. If they wait until after C++23 I guess it could probably be treated as a DR (at least if they choose option 2).

seeplus wrote:
VS2022 intellicode gives warning C26815 - "The pointer is dangling because it points at a temporary instance which was destroyed".

It's an unrealistic example. In a real situation you might get a reference to the vector from a getter function defined in another translation unit so that the compiler is unable to determine that it is going to dangle.
Last edited on
It might even fix some bugs but I'm afraid it might lead to a confusing situation about what rules actually apply (in practice) when using older versions of the standard.


Well we went through that before and escaped reasonably unscathed when they changed their minds 1) about the scope of a variable defined in the init-part of a for loop 2) about what auto a {2} means. I dare say we'd get through another...

In any case, why can't the compiler just emit a warning message in cases of dangling pointers/refs to temps...
Maybe we are talking about different things. When I read "DR" I assumed something like what's described here: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0636r3.html#dr

seeplus wrote:
we went through that before and escaped reasonably unscathed when they changed their minds 1) about the scope of a variable defined in the init-part of a for loop 2) about what auto a {2} means. I dare say we'd get through another...

I don't think the examples you mention were DRs (using the definition of DR in my link). They were just changes they made in a new version of the standard. I think they should probably do the same with this range-based for loop issue, i.e. make the change in C++23 (or C++26) without creating a DR.

seeplus wrote:
In any case, why can't the compiler just emit a warning message in cases of dangling pointers/refs to temps...

It can, if it knows its sometimes or always dangling. The problem is that a member function of a temporary that returns a pointer or reference will not necessarily dangle, because it might for example return a reference to static variable, so if the compiler wants to avoid false positives it would have to analyse the implementation of the function which might not always be possible or feasible.
Last edited on
A few days ago I watched a CppCon video about C++23.

C++23 - What's In It For You?
https://youtu.be/b0NkuoUkv0M

It gives a quick glimpse at many of the new C++23 features but without going into much details.

At the end he tries to answer some questions that I would like to comment about. It probably would be more appropriate to post them on YouTube but I have my reasons for not doing that so I post them here instead.

https://youtu.be/b0NkuoUkv0M?t=3178
At 52:58 there is a question about whether there will be a simple way to iterate over all the keys or all the values, independently, in a std::flat_map. The answer he gets is no but the truth is that you can call the keys() or values() member function to get the container with all the keys or all the values. That'll make it pretty easy to iterate over all the keys or all the values if you want.

https://youtu.be/b0NkuoUkv0M?t=3347
At 55:47 there is a question that essentially asks why we need std::move_only_function. Why can we not just use std::function to store a move-only callable and move it (similar to how std::vector can handle move-only types)? He doesn't know the answer. I think it has something to do with the fact that std::function uses type-earasure so it's impossible to enforce whether std::function can be copied at compile time because it would depend on the callable. Maybe it would have been possible if std::function threw an exception if you tried to copy it with a move-only callable but that has other disadvantages.

https://youtu.be/b0NkuoUkv0M?t=3447
At 57:27 someone claims that the C backtrace function does not work well with static binaries on Linux and asks if std::stacktrace will work better. The answer he gets is that it should, because the standard says so. I think this is slightly incorrect, or at least misleading. The standard says it's "approximate". It could still be affected by optimization flags, the availability of debug symbols, etc. I don't think it will work "better" than anything that is already there. The advantage of std::stacktrace is that we get something standardized that uses the same API everywhere. Some people might not like this but I think it's the only way that makes sense for C++ because we don't want compilers to always generate additional debug symbols that we haven't asked for, and limit optimizations, just so that we can get perfect stacktraces if we happen to request one (it would go against the "don't pay for what you don't use" philosophy).

https://youtu.be/b0NkuoUkv0M?t=3541
At 59:01 he gets a question if he knows why std::unreachable() is a function and not an attribute. He didn't have an answer but he speculated that it might have been better if it was an attribute because if you call std::unreachable at a place where you should not call it then that would lead to UB. I just want to say that that would have been the case even if it was an attribute (Edit: Unless he meant the unreachable attribute would only be usable directly in relation to if/else blocks and case labels. Not sure how that would work). Looking at the original proposal it seems like they preferred a function call syntax because attributes can't be used in expressions. Personally I don't have an opinion.
Last edited on
FYI - the issue of dangling refs is covered in Meyers Book "Effective Modern C++" in Item 31 where he discusses default capture mode in lamdas.
Last edited on
Re the dangling ref code above. This could be coded as:

1
2
3
4
5
6
7
#include <vector>
#include <iostream>

int main() {
	for (const auto& n : static_cast<std::vector<int>>(std::vector<std::vector<int>> { {1,2},{3,4} }[0]) )
		std::cout << n << "\n";
}


which displays the expected:


1
2

seeplus wrote:
the issue of dangling refs is covered in Meyers Book "Effective Modern C++" in Item 31 where he discusses default capture mode in lamdas.

There are many ways to get dangling references. Not sure I think the issue with range-based for is comparable with the dangers of lambda captures. The lambda capture rules are at least intuitive. The range-based for loop issue is unintuitive.

seeplus wrote:
the dangling ref code above. This could be coded as: ...
 
for (const auto& n : static_cast<std::vector<int>>(std::vector<std::vector<int>> { {1,2},{3,4} }[0]) )

Yeah, but now you're making an unnecessary copy. It might be better to store the vector as a local variable and then loop.

1
2
3
4
5
6
7
8
#include <vector>
#include <iostream>

int main() {
	std::vector<std::vector<int>> vec{ {1,2},{3,4} };
	for (const auto& n : vec[0])
		std::cout << n << "\n";
}
Last edited on
Yeah - I didn't say it was the best way... :) :)
Updates on the two NB comments that I mentioned earlier...

It seems like there is still hope to fix the range-based for loop issue already in C++23.
Forward P2644R0 to CWG for inclusion in C++23.
 SF 	F 	N 	A 	SA
 25 	16 	2 	2 	2
Result: Consensus.
https://github.com/cplusplus/nbballot/issues/471#issuecomment-1307722177

Looks like the assume macro is going to be included in C++23 as planned.
Revert P1774 for C++23 as proposed in the National Body comment.
 SF 	F 	N 	A 	SA
 7 	4 	12 	14 	15
Result: Consensus for no change

Amend the specification for [[assume]] to permit a conforming
implementation to syntactically ignore the attribute (AKA token soup),
as proposed in the National Body comment.
 SF 	F 	N 	A 	SA
 8 	6 	4 	20 	9
Result: Consensus for no change

Recommend to Reject
https://github.com/cplusplus/nbballot/issues/425#issuecomment-1308009272
Last edited on
Topic archived. No new replies allowed.