C++ — cast_away_const(..)

The other day I was using const_cast<..>(..) (safely and appropriately, I assure you), and I wondered how difficult it would be to wrap it in a template function. I was only interested in the common simple casting cases:

I did not care about deeper cases such as these.

const_cast<..>(..) will not cast away const buried in function types anyway.

My first thought was to try this:

// first (incorrect) try
template< typename T >
T
cast_away_const( const T a)
{
  return const_cast< T >( a);
}

This does not work for a lot of reasons. Let’s look at some test cases:

// vars with const to be cast
const int   c   = 3;
const int&  rc  = c;
const int*  pc  = &c;
const int*& rpc = pc;

// function that returns an int
const int  get_const_int( );
const int& get_const_ref( );

// Simple ref cast:
// (const int&) --> (int&)
int& r1 = cast_away_const( c);
int& r2 = cast_away_const( rc);
int& r3 = cast_away_const( get_const_ref( ));

// Simple ptr cast:
// (const int*) --> (int*)
int* p1 = cast_away_const( pc);
int* p2 = cast_away_const( rpc);

// Simple ptr ref cast:
// (const int*&) --> (int*&)
int*& rp1 = cast_away_const( pc);
int*& rp2 = cast_away_const( rpc);

// Rvalue ptr cast:
// (const int*&&) --> (int*)
int* p3 = cast_away_const( &c);
int* p4 = cast_away_const( &rc);

// Rvalue ref cast:
// (const int&&) --> (int&)
// This tries to create a ref to an unnamed temp var.
// This is illegal. The compiler should report an error.
int& r4 = cast_away_const( get_const_int( )); /* illegal */

// Rvalue ptr ref cast:
// (const int*&&) --> (int*&)
// These are illegal. They will result
// in a ref to an r-value (an unnamed
// temporary). The compiler should
// refuse these and report an error.
int*& rp3 = cast_away_const( &c); /* illegal */
int*& rp4 = cast_away_const( &rc); /* illegal */

The first try above is wrong even with simple ref and ptr casts. In the simple ref case, T is interpreted as int and cast_away_const(..) returns int. The returned int is stored as a copy in an unnamed temporary variable, also known as an rvalue or r-value. Even if you could take its ref it would be a ref to the wrong variable.

In the simple ptr case, T is interpreted as const int* which is the return type. T is not int* like we hoped. The const is buried under the pointer, but the pointer value itself is not const.

My second try looked like this:

// Second try. Almost right.

template< typename T >
T*
cast_away_const( const T* a)
{
  return const_cast< T* >( a);
}

template< typename T >
T&
cast_away_const( const T& a)
{
  return const_cast< T& >( a);
}

This mostly works. It even correctly reports rp3 and rp4 as errors. But it does not report r4 as an error, and it incorrectly reports rp1 and rp2 as errors. With rp1 it follows this reasoning:

With rp2 it follows the same reasoning. rpc (a const int*&) also uses the pointer version of the function and expands with T as int, so it too creates an rvalue upon return. And r4 is (wrongly) considered OK because get_const_int( ) returns const int which creates an rvalue which is passed to cast_away_const( const T&) where T is int, and which returns a non-const ref to that rvalue (which no longer looks like an rvalue to cast_away_const(..)).

Next we try three overloads, for const T&, const T* and const T*&.

// Third try. Overload conflicts.

template< typename T >
T&
cast_away_const( const T& a)
{
  return const_cast< T& >( a);
}

template< typename T >
T*
cast_away_const( const T* a)
{
  return const_cast< T* >( a);
}

template< typename T >
T*&
cast_away_const( const T*& a)
{
  return const_cast< T*& >( a);
}

This does not work either. Many of the pointers (p1, p2, rp1 and rp2) match both const T* and const T*&, and so the compiler cannot decide which overload to call. But p3 and p4 work correctly, and rp3 and rp4 are both correctly flagged as errors because on those lines cast_away_const(..) is passed an rvalue param (&c or &rc) which only matches const T*. An rvalue will only match a reference to a constant value, and const T* is not a const pointer. It is a non-const pointer to a const T. The rvalues &c and &rc would however match const T* const & since the pointer value is const.

Another thing to try is provide just two overloads, for const T& and const T*&. This does not work for p3 and p4 however, since on those lines cast_away_const(..) is called with const T* rvalues. And rvalues cannot be passed as refs unless the immediate value (the pointer in this case) is const.

All this talk about rvalues suggests a solution however. Provide overloads for const T&, const T*&, and const T* const &.

// Final try.

template< typename T >
T&
cast_away_const( const T& a)
{
  return const_cast< T& >( a);
}

template< typename T >
T*&
cast_away_const( const T*& a)
{
  return const_cast< T*& >( a);
}

template< typename T >
T* const &
cast_away_const( const T* const & a)
{
  return const_cast< T* const & >( a);
}

This works for all the cases except r4. It correctly rejects rp3 and rp4 and accepts everything else. In the r4 case, it sets r4 to be a ref to an unnamed temporary rvalue variable. This is probably safe (depending on context) since the unnamed temp is only destroyed upon local-scope exit, just after r4 is destroyed. I wouldn’t want this in my code, but it is probably safe.

Consider the following statements.

int& r4 = cast_away_const( get_const_int( )); /* not error */

int& r5 = const_cast< int& >( get_const_int( )); /* is error */
int& r6 = const_cast< int  >( get_const_int( )); /* is error */

const
int& r7 = get_const_int( ); /* not error */
int& r8 = const_cast< int& >( r7); /* not error */

Although r4 and r5 are exactly the same, only r5 generates a compiler error. But r8 is also exactly the same and generates no error. This inconsistency arises because you are allowed to bind const int& (r7) but not int& to an rvalue. And then you can cast const int& to int&, making a non-const ref to an rvalue. r4 may be bad style, but it is also just a simpler way of achieving r8.

When C++0x comes out and I get a compiler with rvalues (&&) then I’ll find out more. Will it become illegal to bind rvalues to const T& and const T* const &? It is only legal now as a kludge, because there is no rvalue ref. And how will rvalue refs affect the definition of cast_away_const(..)? I suspect I’ll end up with four overrides, for const T&, const T&&, const T*& and const T*&&. I won’t know for sure until I try it though. I look forward to finding out.

Comments

Leave a Reply