Constant View: A proposal for a std::as_const helper function template

ISO/IEC JTC1 SC22 WG21 N4380 - 2015-02-06


ADAM David Alan Martin (adamartin@FreeBSD.org)
                       (amartin172@bloomberg.net)
Alisdair Meredith      (ameredith1@bloomberg.net)

Table of Contents

  1. Introduction
  2. Background
  3. Motivation
  4. Alternatives
  5. Discussion
  6. Proposed Implementation
  7. Further Discussion

Introduction

This paper proposes a new helper function template std::as_const, which would live in the <utility> header. A simple example usage:

#include <utility>
#include <type_traits>

void
demoUsage()
{
    std::string mutableString= "Hello World!";
    const std::string &constView= std::as_const( mutableString );

    assert( &constView == mutableString );
    assert( &std::as_const( mutableString ) == mutableString );

    using WhatTypeIsIt= std::remove_reference_t< decltype( std::as_const( mutableString ) >;

    static_assert( std::is_same< std::remove_const_t< WhatTypeIsIt >, std::string >::value,
            "WhatTypeIsIt should be some kind of string." );
    static_assert( !std::is_same< WhatTypeIsIt, std::string >::value,
            "WhatTypeIsIt shouldn't be a mutable string." );
}

Background

The C++ Language distinguishes between 'const Type' and 'Type' in ADL lookup for selecting function overloads. The selection of overloads can occur among functions like:

int processEmployees( std::vector< Employee > &employeeList );
bool processEmployees( const std::vector< Employee > &employeeList );

Oftentimes these functions should have the same behavior, but sometimes free (or member) functions will return different types, depending upon which qualifier (const or non-const) applies to the source type. For example, std::vector< T >::begin has two overloads, one returning std::vector< T >::iterator, and the other returning std::vector< T >::const_iterator. For this reason cbegin was added to the container member-function manifest.

Motivation

A larger project often needs to call functions, like processEmployees, and selecting among specific const or non-const overloads. Further, within C++11 and newer contexts, passing an object for binding or perfect forwarding can often require specifying a const qualifier applies to the actual object. This can also be useful in specifying that a template be instantiated as adapting to a "const" reference to an object, instead of a non-const reference.

Alternatives

  1. Continue use the hard-to-use idiom const_cast< const T & >( object )
  2. Use a more modern idiom const_cast< std::add_const< decltype( object ) >::type & >( object )
  3. Provide a language extension, i.e.: const_cast( object ) which is equivalent to the above
  4. Provide a modified cast form, i.e.: (const) object which is equivalent to the above
  5. Provide a new trailing cast-like form, i.e.: object const, or object const.blah or object.blah const
  6. Provide a library function, as_const, which this paper proposes

Discussion

This conversion, or alternative-viewpoint, is always safe. Following the rule that safe and common actions shouldn't be ugly while dangerous and infrequent actions should be hard to write, we can conclude that this addition of const is an operation that should not be ugly and hard. Const-cast syntax is ugly and therefore its usage is inherently eschewed.

Regarding the above alternatives, each can be discussed in turn:

const_cast< const T & >( object ):

The benefits of this form are:

The drawbacks of this form are:

const_cast< std::add_const< decltype( object ) >::type & >( object ):

The benefits of this form are:

The drawbacks of this form are:

Extend the language with const_cast( object ):

The benefits of this form are:

The drawbacks of this form are:

Extend the language with (const) object:

The benefits of this form are:

The drawbacks of this form are:

Library function, std::as_const( object ):

The benefits of this form are:

The drawbacks of this form are:

In proposing std::as_const( object ), we feel that the name is sufficiently terse yet descriptive. Every other name had at least one drawback.

Proposed Implementation

In the <utility> header, the following code should work:

// ...
// -- Assuming that the file has reverted to the global namespace --
namespace std
{
    template< typename T >
    inline typename std::add_const< T >::type &
    as_const( T &t ) noexcept
    {
        return t;
    }

}
// ...

Further Discussion

The above implementation only supports safely re-casting an l-value as const (even if it may have already been const). It is probably desirable to have xvalues and prvalues also be usable with as_const, but there are some issues to consider. Some examples of contexts we'd like to consider for an expanded proposal are:

std::string getString();
auto &x= as_const( getString() );
auto &x= const_cast< const std::string & >( getString() );


void function( std::string & );
void function( const std::string & );

function( as_const( getString() ) );

An alternative implementation which would support all of the forms used above, would be:

template< typename T >
inline const T &
as_const( const T &t ) noexcept
{
    return t;
}

template< typename T >
inline const T
as_const( T &&t ) noexcept( noexcept( T( t ) ) )
{
    return t;
}

We believe that such an implementation helps to deal with lifetime extension issues for temporaries which are captured by as_const, but we have not fully examined all of the implications of these forms. We are open to expanding the scope of this proposal, but we feel that the utility of a simple-to-use as_const is sufficient even without the expanded semantics.