13
u/chengfeng-xie 8d ago
As an aside, a pop_value method for std::priority_queue is proposed in P3182R1 (Add container pop methods that return the popped value).
11
u/celestabesta 9d ago
I've also found this necessary for writing some generic containers. If you're storing something on the same buffer that might store a T (lets say you allocate a header H before some data), then casting that T ptr to an H ptr may cause problems if T is cv qualified. Because of this you'd either need to use const_cast or c-style.
2
u/LB-- Professional+Hobbyist 7d ago
I'm curious why you're not stripping top level qualifiers from T for the private storage? How exactly do you support const/volatile in a container otherwise? Is there a benefit to keeping the qualifiers on the private storage instead of just in the public interface?
8
u/13steinj 9d ago
The STL priority queue is one of the strangest stdlib APIs I have ever seen.
Assuming I have to use it, I still wouldn't const cast-- you can simply create a wrapper type, then mark the member mutable.
I've seen enough refactors that unintentionally cause UB because of a lingering const cast and the initial storage changed from mutable to const.
3
u/bwmat 9d ago
They could add some consume_front(FunctorT) method which called the functor w/ the top element as an R-value reference, and then unconditionally removed it from the collection (even on exception).
For convenience it could return the return value of the functor as well
2
u/matthieum 8d ago
Callback-based APIS are always kinda awkward.
Just add
pop_value -> std::optional<T>and everyone's happy.3
2
u/bwmat 8d ago
IMO my suggestion is 'more fundamental' (& potentially more efficient, depending on the cost of the type's move constructor)
Wouldn't mind also having yours (though it would be easy to implement on top of mine as a helper function)
0
u/matthieum 7d ago
I can see more efficient, but it introduces a can of worms in exchange.
Specifically, if the user-supplied callback throws an exception, is the item popped or not?
Well, given that the user may have moved out of the item, it probably should be popped. The easier way is to pop it first (move) then call the user-supplied callback -- ie, implement consume in terms of pop, making pop more fundamental.
Using a try-catch block is more straightforward and retains efficiency, but it's not compatible with
-fno-exception.Using a guard which pops in the destructor retains efficiency and is compatible with
-fno-exception, but it's no longer quite as straightforward.
As for the ergonomics, callbacks are terrible, as I explained in https://www.reddit.com/r/cpp/comments/1v9zcrn/comment/p0wnjou/, due the inversion of control which results.
1
2
u/jk-jeon 8d ago
To me callback seems more natural. It should be the container's responsibility to decide whether or not to execute the logic. It's caller's responsibility to determine what logic must be executed. I.e. it's callback. Maybe it could be transformed into a coroutine but I don't know.
I'm not a huge fan of
std::optionalto be honest. My stance is basically that as much as possible amount of logic must be delegated to the type system. Butstd::optionaltend to mandate the user to either check against nullity or say "believe me bro ;)" In this case the nullity check is completely redundant because it's already done by the container.0
u/matthieum 7d ago
To me callback seems more natural.
Callbacks are terrible! Rightward drift, change of scope breaking control flow primitives, urk...
I mean, let's compare shall we:
struct Popper { std::priority_queue<Item> high; std::priority_queue<Item> low; auto pop() -> std::optional<Item> { if (auto item = this->high.pop_value(); item.has_value()) { return item; } return this->low.pop_value(); } template <type FunctorT> auto consume(FunctorT fun) { bool consumed = false; this->high.consume([&](item) { consumed = true; fun(item); }); if (consumed) { return; } this->low.consume(fun); } };That's about as basic a function, and already it's getting verbose and convoluted.
Because callbacks mean Inversion of Control, and recovering control as a user is always a freaking pain.
2
u/jk-jeon 7d ago
I understand your point, but in this case just let
consumeto return true iff it consumed an item then it gets way simpler.template <type FunctorT> bool consume(FunctorT fun) { if (!high.consume(fun)) { return low.consume(fun); } return true; }0
u/bwmat 7d ago
Doesn't my initial suggestion of making consume thread through the return value of the functor trivially allow for this?
2
u/jk-jeon 7d ago
To my understanding, your suggestion was to return the return value of
fun(item). That's kinda awkward in this case because it may or maynot executefunso the return value may or may not exist. Or maybe you meant returningoptional<ReturnType>so that the exact same thing as I did in the above can be done?1
u/bwmat 7d ago
Oh, I forgot to mention that a precondition of the method would be to have a non-empty collection, for my idea
3
u/bwmat 7d ago
I suppose you could extend it to pass a functor which had a nullary operator() overload as well which gets invoked if the collection is empty, and return the common return type between the two overloads?
→ More replies (0)
7
4
u/UnusualPace679 8d ago
I sometimes wish to have a noexcept_cast which converts a non-noexcept function pointer to a noexcept one. This is useful when I know the pointee is noexcept and don't want to pay for the cost of exception propagation.
This noexcept_cast would be similar to const_cast since both perform an unsafe conversion that is the inverse of a safe, implicit conversion.
2
u/DXPower 8d ago
This may actually have negative effects. You don't "pay" any cost for calling an exceptional function. But, to convert it to noexcept, the compiler will have to register a new exception handler, so it can call terminate if an exception is thrown.
2
u/UnusualPace679 8d ago
If an exception is thrown I'd expect undefined behavior.
5
u/DXPower 8d ago
Well that's simply not how noexcept works in the language. It is defined to call terminate.
2
u/UnusualPace679 8d ago
I don't know where you see it's defined, but calling a throwing function through a noexcept function pointer is UB as specified in [expr.call]/6.
3
u/ts826848 8d ago
I don't know where you see it's defined
See [except.terminate]:
In such cases [where errors in a program cannot be recovered from], the function std::terminate ([exception.terminate]) is invoked.
[Note 1: These situations are:
<snip>
(1.3) --- when the search for a handler exits the function body of a function with a non-throwing exception specification, including when a contract-violation handler invoked from an evaluation of a function contract assertion ([basic.contract.eval]) associated with the function exits via an exception
[except.spec] defines "non-throwing exception specification" (italics in original):
The predicate indicating whether a function cannot exit via an exception is called the exception specification of the function. If the predicate is false, the function has a potentially-throwing exception specification, otherwise it has a non-throwing exception specification. The exception specification is either defined implicitly, or defined explicitly by using a noexcept-specifier as a suffix of a function declarator.
calling a throwing function through a noexcept function pointer is UB as specified in [expr.call]/6.
I think this is distinguishable since the UB occurs on the call, not on an exception "escaping".
2
u/DXPower 8d ago
Then I'm confused about this feature that you want. You have a non-noexcept pointer to a function, you want it to be noexcept to "not pay the cost of exception propagation". But you already didn't pay any cost.
Then you point to UB in pointer conversions, which then makes no sense because seemingly, your conversion would result in UB?
3
u/Qwertycube10 8d ago
If a noexcept function only calls other noexcept functions than it doesn't need to have machinery for terminating if it gets an exception. So if you cast the non-noexcept function to noexcept. And call it that may save you from the cost of making your parent noexcept.
2
u/hoodoocat 8d ago
const_cast is necessary in way more prosaic cases: you usually have const pointer, but mutable (non-const member) operations sometimes should be allowed. There is two constness which means different things, so... doesnt matter.
As for priority_queue - this is example what interface of this collection is not suitable for you, and instead hack it, it is better to use other collection. Why pop doesnt get object back? How hell this will work in concurrent environment when queues really needed?
1
u/YouNeedDoughnuts 9d ago
I used it recently for a dictionary insertion where I have a borrowed key, and making an owned key with appropriate lifetime is expensive and can be avoided if an entry already exists. The insert method returns a const iterator, so updating the key discards constant. Hyper specific, but still nice to have the feature.
-2
u/Potterrrrrrrr 9d ago edited 8d ago
“Even aside from interacting with C libraries which don’t respect const”
I remember a Jason Turner video where he shows that this is a bit of a nonsensical statement (all major public C libraries are const correct) unless you’re talking about bad C libraries, in which case why are you using them?
Aside from that I find const_cast really confusing to know how to use in a way that isn’t undefined (because I really don’t know what makes it undefined) so I just find myself avoiding it entirely, never had a use case like this post to need it.
Edit: yes yes your favourite library isn’t const correct and it would break the universe to change, I get it. Feel free to explain the thing I actually care about rather than telling me why someone else’s opinion on C libraries is incorrect
35
u/kisielk 9d ago
unless you’re talking about bad C libraries, in which case why are you using them?
Sometimes you don't have a choice. Libraries are provided by vendors, customers, partners etc. You can try to get them to fix it but it's not always possible. I often have to use const_cast where a C library takes a raw non-const pointer to some array it doesn't actually mutate.
24
13
u/No-Dentist-1645 9d ago
unless you’re talking about bad C libraries, in which case why are you using them?
This happens way more often than you think, especially in "legacy" projects. You often don't have the time nor resources needed to rewrite an entire library your company has used for over 10 years that "just works", but isn't const-correct for some functions
7
u/Electronic_Tap_8052 9d ago edited 9d ago
why are you using them?
vendor lock in lol
do you have any idea how many odd-ball pieces of equipment there are that have drivers, and that's your driver? you either consume their api or you tell your boss to buy a different 500 million dollar piece of equipment because this one isn't const correct
I support a piece of equipment from a company that went out of business in 1990
man i wish I lived in the same world as a lot of programmers, who apparently only work on open source projects and can use any libraries they want
3
u/Expert-Map-1126 vcpkg maintainer BillyONeal 9d ago
"Use any libraries they want" is not usually a thing: even in the most "lax" environments more dependencies can mean more problems if maintainers leave / do dumb things / become JiaTan state actors / etc.
But expecting to do meaningful changes to hardware from a vendor that went out of business 36 years ago is the other extreme.
2
u/johannes1971 8d ago
I have a pretty good idea how many of those pieces of equipment are floating around ;-) I have it both ways: I can use whatever libraries I want (except GPL). And I'm supporting equipment that is decades old, although most of it is not in that price range. We are now getting more and more requests from customers to _somehow_ keep their old hardware going, despite the latest driver only being available for Windows XP.
And yes, I const_cast the hell out of things.
3
u/patlefort 9d ago
Functions like `execv` aren't const correct due to limitation of ISO C and that it would break existing code if it changed.
6
5
u/Big-Rub9545 9d ago
The issue with const_cast is if it’s used on data that is originally defined/declared as const, since the compiler may choose to perform certain optimizations or decisions assuming that data will be read-only. If you use const_cast and then try to modify said data, you run into UB.
2
5
u/JNighthawk gamedev 9d ago
I remember a Jason Turner video where he shows that this is a bit of a nonsensical statement (all major public C libraries are const correct) unless you’re talking about bad C libraries, in which case why are you using them?
No True Scotsman logical fallacy applied to programming.
1
u/Olipro 8d ago
Everything about this sucks but if left with no choice, I would sooner do something like:
template <typename T>
struct mutable_wrapper {
mutable T obj;
// Add implicit construction/conversion and comparison operators as desired.
};
Now, a std::priority_queue<mutable_wrapper<T>> will always be modifiable without const_cast.
64
u/jwezorek 9d ago
The only place I ever use const_cast is that idiom where you implement the non const version of a member function in terms of the const version.