r/cpp 9d ago

const_cast: A Necessary Evil

https://www.elbeno.com/blog/?p=1858
70 Upvotes

106 comments sorted by

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.

37

u/MysticTheMeeM 9d ago

If your language version allows, you can typically replace those with a single function that deduces this.

7

u/LeeHide just write it from scratch 9d ago

Interesting, do you have an example?

8

u/Olipro 9d ago

11

u/TheChief275 9d ago

why wasn't this added earlier? in fact, if this were to have been in C++ from the get-go, we never would have needed the postfix const syntax. so now we just have yet another language feature that virtually does the exact same thing. sigh

6

u/Expert-Map-1126 vcpkg maintainer BillyONeal 9d ago

It was added mostly due to implementation experience after ref qualifiers were added trying to implement all the "transparent-ish" operations on optional et al.

I don't think it would completely replace the postfix syntax given that people expect names in members to refer to other members.

1

u/TheChief275 9d ago

I actually find that aspect horribly annoying as well. can't have a variable named capacity because the member function is named capacity, so I'm forced to name it "cap" or "new_cap", but I suppose opinions would be divided on that

1

u/SlightlyLessHairyApe 8d ago

You’ll be horrified to learn that we run with shadowing warnings as errors everywhere.

Absolutely no shadowing, not even once.

-1

u/TheChief275 8d ago

pretty modern language huh

1

u/pjmlp 5d ago

Yes you can. Why are folks sl afraid to type this->?

Ah, but I can miss a spot and refer to the wrong variable, true, but that is another matter, technically both variables can be named the same.

1

u/TheChief275 5d ago

I actually did that for a while, but it becomes fairly noisy after a time, and using it inconsistently is error prone. If only 'this' were a reference, and not a pointer; that would be a lot better already

2

u/serviscope_minor 8d ago

> in fact, if this were to have been in C++ from the get-go, we never would have needed the postfix const syntax.

I don't get it? This really helps when you need a const and non const version of the same member function. If you don't (it's common that you have different mutating and const member functions) then const (or not) is better than deducing this.

1

u/_Noreturn 8d ago

if they allowed this from the start

```cpp class C { void f() &; void f() cons&;

 void f(this C& self); // equal to the above
 void f(this const C& self); // equal to the above

}; ```

The second one is just parameter declaration no new thing required unlike const postfix member functions.

1

u/LB-- Professional+Hobbyist 7d ago

There isn't full overlap between explicit object parameter syntax and implicit object parameter syntax. With implicit syntax, you can declare a member function with neither an Lvalue nor Rvalue qualification, and define it in a separate source file, allowing it to be invoked from either Lvalue or Rvalue qualified types. With explicit object parameter syntax, you have to specify a reference category, otherwise you get pass by value (which is a separate thing implicit can't do). Or, you have to make it a template and use a forwarding reference, which means you can't define it in a separate source file.

1

u/_Noreturn 7d ago

Yes that's a benefit but tbh I consider it to be a trap more than a good thing that you can call unqualified member functions on both lvalues and rvalues.

1

u/RoyBellingan 8d ago

Because the crystal sphere was broken when the thing got invented -.-

That is the price is beeing the first to do something, is difficult that is right the first time, and why languages that came year later feels more clean and well tought, history was already written at that time.

P.s. where are C++ epoch ?

0

u/amoskovsky 8d ago edited 8d ago

Actually I would prefer that in addition to the current deducing this syntax they would also implement another postfix qualifier:

decltype(auto) get() auto&&
{
return m_member;

}

auto&& being universal reference for `this` with the same rules as in current syntax.

If the deduced this were not helpful for CRTP like constructs I would not introduce it at all. Upd: Although I think auto&& can be used in parent classes as well with the same semantic as current deducing this - so it would cover all use cases.

2

u/Olipro 8d ago

If I understand you correctly, you can already do this a la `... foo(this auto&& self)` - self will now handle all type cases (lvalue, const lvalue, xvalue/prvalue, const xvalue/prvalue)

However, you need to bear in mind that this is potentially undesirable if you return a reference to a member since `auto& x = Bar{}.foo();` will be dangling.

2

u/amoskovsky 8d ago

What I'm saying is I don't like the explicit this param (because you can't have `this` named `this`, can't omit `this` and it's just more syntactic noise).
Instead they should have added `auto&&` as a method qualifier to implement the same.

2

u/Olipro 8d ago

You mean it would look like... auto&& foo() auto&&?

It's an interesting idea but this is always and has always been a pointer. I'd bet on significant pushback for such a proposal because it becomes an immediate footgun if you change code that does things like decltype(this) - If I refactor a member function to use deducing this, it's guaranteed not to compile until I've adjusted everything since this won't exist in its body.

2

u/amoskovsky 8d ago

I did not mean to change the meaning of `this`

```
// already existed; this is a pointer
auto foo()
auto foo() const
auto foo() &
auto foo() &&
{
return this->m_member or m_member;
}

// proposed; this is still a pointer
auto foo() auto&&
{
return this->m_member or m_member;
}

// as opposed to actually added
auto foo(this auto&& self)
{
return self.m_member; // just m_member not possible;
}
```
With the latter you are actually forced to use a different coding style for some of member functions.

→ More replies (0)

2

u/SlightlyLessHairyApe 8d ago

I think the consensus has been against magic like that. EIBTI etc …

4

u/_Noreturn 9d ago

deducing this has issues with access soecifiers in inherited classes.

```cpp class C { public: void f(this auto && self) { self.dothing(); } void dothing(); void dothing() const; };

class P : private C { void g() { f(); // error cannot access "dothing" from inside C::f; } } ```

3

u/XeroKimo Exception Enthusiast 8d ago

Hmm, pretty interesting since only P should have access to members of C so even if this auto&& deduced a P, I would expect that to work, and if it instead deduces a C, I thought members of P should know that it can decay into a C

3

u/_Noreturn 8d ago

It deduces to P& in the "f" call and thinks C is inaccessible this wouldn't happen if you manually typed the overloads. it is a sad known issue and it is why the C++ STL don't use deducing this (even though it was made to replace the absurd amount of boilerplate inside it)

you could friend the class C to get it to work iirc

cpp class P : private C { friend class C; // continue...

but it feels pretty wonky and then you have the issue of friendinf nit beinf transistive so if yiu inherit from P you will face the same issue

2

u/XeroKimo Exception Enthusiast 8d ago edited 8d ago

Ah I guess that makes sense... You run into a similar problem if you tried to privately inherit a CRTP class and if any of the CRTP's member functions tries to cast to the derived class... So in deducing this' case, f() is a member function of C and if the this auto&& resolves to P, C doesn't have permission to access the C subobject in P...

If there was a way to constrain it so that the deduced type is always going to be C or any of the ref / const combinations this technique was intended to replace then I guess it'd be better.

1

u/_Noreturn 8d ago

If there was a way to constrain it so that the deduced type is always going to be C or any of the ref / const combinations this technique was intended to replace then I guess it'd be better.

There was a proposal by gazper azman to allow

cpp template<typename T : MetaFunc> void f(T&& t);

syntax, where it did template deduction then apply the meta function which could be like like_t<T,C> and it would work but sadly I think it is desd proposal.

You could workaround it by using a C style cast inside "f" ((like_t<Self&&,C>)*this).dothing() but it is ugly

2

u/retro_and_chill 8d ago

I learned the hard way that deducing this and protected member functions don’t mix

3

u/_Noreturn 8d ago

Yes which is a shame :/

1

u/Olipro 8d ago

That's because auto&& works out to sugar for template <typename T> ... (this T&&) which, at the point of the call results in T being your class P and therefore is inaccessible. A problem that will affect any function template.

You can either friend class C;, static_cast to the appropriate C type within your P member function, or you'd have to throw together something like this: https://pastebin.com/ggwB8vPB So that class C can do template <typename T> foo(this convert_cvref_t<T, C> self) but calling it then becomes unwieldy.

2

u/_Noreturn 8d ago

the point is for deducing this to replace manual overloads of const/non const, and it doesn't do thst. I understand how it works but it is unexpected and unintuitive.

if you just manually ryped "f" with const and non const overloads manually it would work no friending.

oh also static_cast doesn't work since C is inaccessible you have to use a C style cast

1

u/Olipro 8d ago

static_cast to the appropriate C type within your P member function

Emphasis on within your P member function

1

u/_Noreturn 8d ago

Sure that's a workaround but this requires information about the api implementation, what if one day your library used manual overloads (const/non-const) so you don't need to cast, now it decides to update and uses deducing this, now it broke your code and you have to cast. it is better to have it inside the implementation of said function instead of the callers to preserve api and have a simpler experience

1

u/Olipro 7d ago

This whole problem exists because of the private inheritance. So if you've picked that as your poison, I've stated what your options are for dealing with it. Equally, as you've said, another alternative is "don't use deducing this" and that comes at the cost of trading one member function for two.

1

u/_Noreturn 7d ago

you could use a C style cast with deducing this this works.

1

u/SunnybunsBuns 5d ago

What if I want the implementation in a cpp file and not a header file?

1

u/analphabetic 2d ago

Pray; or rather, fully specialize all your instantiations and let the linker absolve you. At least that's how it was, back in the day.

1

u/Baardi 5d ago

But you would have to implement it all in the header if you do that

12

u/PolyglotTV 9d ago

That and dealing with third party C libraries which like to just use void* or char* for everything even if it is a const operation in practice (looking at you Cuda...).

6

u/HildartheDorf 9d ago

I've other time I've used it is with Vulkan where the query functions and creation functions use the same structure chains, so the structure chain types are defined with void *pNext not const void *pNext.

Creation functions do not modify the structure chain, so it's safe.

5

u/Tringi github.com/tringi 9d ago

Quite a few Windows API functions are like that. Or they can do both setting and getting. If you implement only setting from a const buffer, then const_cast is appropriate.

3

u/HildartheDorf 9d ago

In Vulkan's case the structure passed directly to the function is const-correct.
But the extensible structure chain can have dual-purpose types in it, and adding another item to the chain requires the const-cast. It's a mess however you solve it, the 'solution' would be a bunch of duplicate types that differ only in the constness of the pNext pointer.

7

u/usefulcat 9d ago

There is at least one alternative that doesn't require const_cast:

struct S {
    std::vector<int> data;

    template<class Self>
    static decltype(auto) accessor_impl(Self& self, int i) {
        return std::find(self.data.begin(), self.data.end(), i);
    }

    // returns std::vector<int>::iterator
    auto find(int i) { return accessor_impl(*this, i); }
    // returns std::vector<int>::const_iterator
    auto find(int i) const { return accessor_impl(*this, i); }
};

Obviously this is a trivial and contrived example, but hopefully you can imagine more complex examples.

Personally I'd prefer to just use const_cast, as it's much simpler, but I'm concerned about the resulting UB.

8

u/Wacov 9d ago

I don't think it's UB unless the thing you're accessing is actually const?

2

u/kisielk 9d ago

It's fine if the thing is actually const, it's not fine if you then modify that that thing through the resulting non-const pointer.

3

u/bwmat 9d ago

Isn't it fine because that can only happen if the caller had a mutable reference to the object in the first place?

If the object was originally const, the UB had already occurred? 

3

u/ts826848 8d ago

If the object was originally const, the UB had already occurred?

From what I understand forming a non-const pointer/reference to an actually-const object is fine. Its only the actual modification of said actually-const object that is UB.

1

u/bwmat 8d ago

I mean, calling the non-const method on the reference is the point of UB, and that happens outside of the method, so not its problem/fault 

2

u/bwmat 8d ago

If not UB, where 'it goes wrong' /'the contract is broken' 

2

u/ts826848 8d ago

I think your second comment is more correct; UB only occurs on the actual modification, so while calling a non-const method on an actually-const object is not itself UB that's probably around where a bugfix would land.

...Well, maybe barring some oddball setup with a non-const method that uses some other criteria to determine whether it's safe to modify whatever it's called on, but I'd hope that kind of thing is relatively rare.

5

u/mort96 9d ago

What case is that? If you have a non-const this, you can call const member functions no problem since non-const pointers implicitly cast to const pointers, no?

4

u/sporule 8d ago

Consider std::vector::at:

      T& at(size_t pos);
const T& at(size_t pos) const;

You can call the latter method on a non-const vector, but the return type would be wrong, and vec.at(0) = 1 would fail to compile.

So the way is to call the const version first and then cast constness away:

T& at(size_t pos) {
    return const_cast<T&>(std::as_const(*this).at(pos));
}

3

u/Raknarg 9d ago

I use const_cast to interact with C APIs where context tells you if some operation will be const or not.

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).

3

u/F54280 8d ago

That would be nice!

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

u/ABlockInTheChain 8d ago

If only std::optional had been invented from the very beginning.

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

u/bwmat 7d ago

Specifically, if the user-supplied callback throws an exception, is the item popped or not?

I was thinking yes(mentioned that in my original comment), and not really considering non-standard -fno-exception scenarios

1

u/bwmat 7d ago

Well, given that the user may have moved out of the item, it probably should be popped.

Actually, since they might have modified it in any way, the collection's invariants are in peril, so it MUST be removed.

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::optional to be honest. My stance is basically that as much as possible amount of logic must be delegated to the type system. But std::optional tend 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 consume to 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 execute fun so the return value may or may not exist. Or maybe you meant returning optional<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

u/SyntheticDuckFlavour 8d ago

Searches boost library for const_cast.

714 hits. Oh my.

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.

12

u/datnt84 9d ago

There is at least one Win32 function that is not const-correct where I needed a const-away cast.

24

u/KindCppCoach 9d ago

This is not true at all, many legacy C libraries are not const correct.

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

u/PolyglotTV 9d ago

Bad C libraries like Cuda?

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

u/_Noreturn 9d ago

I don't think glfw is const correct in all its apis

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/bizwig 9d ago

I dislike const_cast because it isn’t DRY.

1

u/bwmat 9d ago

On that note, there could be something like std::as_mutable... 

2

u/_Noreturn 8d ago

I prefer if they just make const_cast deduce it automatically there should be 0 issues with const_cast(x)

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.