3

I'm providing a const and non-const variation of a member function where I reuse the const version to implement the non-const version as described in this answer per Scott Meyers books.

The const version takes an argument of type:

const std::function< void (const Foo &) > &

vs the non-const takes an argument of type:

const std::function< void (      Foo &) > &

In the implementation, I have to use reinterpret_cast because const_cast is not working.

E.g.,:

const std::function< void (Foo &) > & Processor;
reinterpret_cast< const std::function< void (const Foo &) > & >( Processor );

vs

const std::function< void (Foo &) > & Processor;
const_cast< const std::function< void (const Foo &) > & >( Processor );

Wouldn't this be in the spirit of what const_cast is meant for? Is this just an oversight in the language definition to perhaps be fixed in C++2x or would const_cast never be in the spirit of things here?

Here is more complete code:

void Collection::ProcessCollection(const std::function< void (const Foo &) > & Processor) const
{
    for( int idx = -1 ; ++idx < m_LocalLimit ; )
    {
        if ( m_Data[ idx ] )
        {
            Processor( m_Data[idx] );
        }
    }

    const int overflowSize = OverflowSize();

    for( int idx = -1 ; ++idx < overflowSize ; )
    {
        Processor( (*m_Overflow)[ idx ] );
    }
}

void Collection::ProcessCollection(const std::function< void (Foo &) > & Processor)
{
    const Collection * constThis = const_cast< const Collection * >( this );
    const std::function< void (const Foo &) > & constProcessor
        = reinterpret_cast< const std::function< void (const Foo &) > & >( Processor );

    constThis->ProcessCollection( constProcessor );
}
WilliamKF
  • 41,123
  • 68
  • 193
  • 295
  • 4
    The `const_cast` applies to the type, not to a subtype as part of the signature of the parameters. – Eljay Jun 13 '19 at 23:24
  • In the approach suggested by Scott, you `const_cast` the arguments so you can call the function. Your code attempts to cast the function type (which will never be blessed by the standard). – M.M Jun 14 '19 at 00:22
  • @KamilCuk Question updated. – WilliamKF Jun 14 '19 at 00:32
  • @M.M Fixed mismatched `< >` and full functions provided. – WilliamKF Jun 14 '19 at 00:41
  • consider having `ProcessCollection` be a function template accepting a Callable . At the cost of having code in the header, it's more concise, flexible and optimizable – M.M Jun 14 '19 at 00:55
  • @M.M That's what I'm trying due to the comments in the answer. – WilliamKF Jun 14 '19 at 00:56

2 Answers2

4

Generally speaking, it's not safe to use const_cast to cast away constness that appears in template arguments. For example, consider this (admittedly, somewhat contrived) code:

template <typename T> struct Wrapper {
    int x;
};

template <> struct Wrapper<char *> {
    double y;
};

Here, a pointer to a Wrapper<const char *> points to a very different object than a Wrapper<char *>, so doing a const_cast to turn a Wrapper<const char *> * into a Wrapper<char *> * would result in a pointer to a struct containing an int now pointing at a struct containing a double, breaking some language rule whose name eludes me at the moment. :-)

Since in general it's not safe to const_cast this way, the language spec doesn't allow for const_cast to be used like this, which is why in your case, even though the operation makes intuitive sense, the language disallows your code with const_cast.

I am fairly certain that using a reinterpret_cast here leads to undefined behavior, since the language considers std::function<T1> and std::function<T2> to be different, incompatible types when T1 and T2 aren't the same. It may happen to work on your system by pure coincidence, but I don't believe you can safely assume this will work.

templatetypedef
  • 362,284
  • 104
  • 897
  • 1,065
  • If not safe, how would I go about implementing a non-const variant in terms of a const one then in this situation? – WilliamKF Jun 14 '19 at 00:34
  • You could have the non-const one pass in a lambda that does a const_cast to strip constness from its argument, then forwards it along to the function that was passed in as an argument, perhaps? – templatetypedef Jun 14 '19 at 00:42
  • 2
    @WilliamKF easiest solution is not to use `std::function` but make your method template. It would be more efficient as well as `std::function` has overhead. – Slava Jun 14 '19 at 00:42
  • Using a lambda sounds like it would be expensive. This code is in a critical loop, so it sounds like I should get rid of `std::function` even and templatize the function on the `Processor` function. – WilliamKF Jun 14 '19 at 00:47
1

reinterpret_cast here is undefined behavior. const_cast isn't appropriate, because the constness of template parameters isn't something you can cast away.

The easy way to solve this is, well, don't. Get rid of some references. Swap who implements it.

void Collection::ProcessCollection(std::function< void (const Foo &) > Processor) const
{
  Collection * mutableThis = const_cast< Collection * >( this );

  mutableThis->ProcessCollection( Processor );
}

void Collection::ProcessCollection(std::function< void (Foo &) > Processor)
{
  for( int idx = -1 ; ++idx < m_LocalLimit ; )
  {
    if ( m_Data[ idx ] )
    {
      Processor( m_Data[idx] );
    }
  }

  const int overflowSize = OverflowSize();

  for( int idx = -1 ; ++idx < overflowSize ; )
  {
    Processor( (*m_Overflow)[ idx ] );
  }
}

std::function stores things that are call-compatible with it.

If you take a Foo&, you can call a function expecting a Foo const& with it. So you can store a std::function<void(Foo const&)> within a std::function<void(Foo&)>.

Wrapping something in a std::function can involve allocating. So you may want to find a high-quality function_view<Sig> to replace your use of std::function.

In another comment, you state that this code is in a critical loop. Eliminating std::function entirely is a good move, or at least reducing the type erasure taps and passing batches of data to it somehow.

The inversion of const/unconst is still appropriate; we implement const in terms of mutable instead of mutable in terms of const because one is a covariant operation, the other is a contravariant operation. See here.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I'm using a lambda now instead of `std::function`, resulting in a significant speedup. See related [question here](https://stackoverflow.com/q/56602770/115751). – WilliamKF Jun 19 '19 at 20:41