Wednesday, March 27, 2013

make_unique (Part 1)

So I went to Herb Sutter's page after watching some videos about  C++11.  This post mentions make_unique, and I wanted to see how far I could take it.  My code is a little more spaced out, so that when the syntax highlighter in my IDE barfs, I can still read it rather easily.  I also prefer the new suffix return syntax for any type longer than six characters, it keeps all the function names lined up on the left.


Part 1 - The Basics

The original version from the post does perfect forwarding of the function parameters to the constructor. No temporaries are made, unless the constructor has pass-by-value parameters, and it keeps the user from calling new, which can produce leaks in the face of unordered execution with exceptions.
template< typename T, typename ...Args >
std::unique_ptr< T > make_unique( Args&& ...args )
{
  return std::unique_ptr< T >( new T( std::forward< Args >( args )... ) );
}
This works for basic objects, until our friend the array comes into play.  There are two types.
First, fixed size arrays: int[ 3 ]
  1. complete type
  2. cannot be copied or assigned to directly
  3. decays to a pointer of the sub-type
  4. addressable (can pass by reference or address)
The last two bullets are the ones that we care about.  I hadn't really thought about number 4, because in general number 3 is what is typically normal, but here's an example.
// don't do this
int bad( int    a  [ 3 ] ) { return sizeof( a ); }
int bad( int ( &a )[ 3 ] ) { return sizeof( a ); }
// do this
int good( int   *a        ) { return sizeof( a ); }
int good( int ( *a )[ 4 ] ) { return sizeof( a ); }

void test()
{
  int a[ 4 ] = {};
  good( a );
  good( &a );
  // bad( a ); // ambiguous, can decay to a pointer or pass by reference
  auto b = a; // b is an int*
  good( b );
  bad( b ); // calls the first bad() due to decayed parameter type
}
Each function above always returns the size of a pointer, because once a enters the function it immediately decays.  clang even warns us about the first bad calling sizeof( a ).
Next, variable size arrays: int[]
  1. incomplete type (only useful in a template parameter)
  2. decays to a pointer of the sub-type
  3. ???
Example:
template< typename T >
auto f( T val ) -> std::pair< bool, bool >
{
  return {
      std::is_array< T >::value,
      std::is_array< decltype( val ) >::value };
}

void test()
{
  int a[ 4 ] = {};
  auto b = f( a );
  auto c = f< int[] >( a );
}
In the above b = { false, false } and c = { true, false }, because f( a ) calls f< int* >( int* val ) and f< int[] >(a) calls f< int[] >( int* val ). Now with that out of the way let's get on with fixing up make_unique.


Part 2 - The Easy Stuff

Let's fix up the original so that it doesn't bother with arrays:
template< typename T, typename ...Args,
          typename = typename std::enable_if<
            !std::is_array< T >::value >::type >
auto make_unique( Args&& ... args ) -> std::unique_ptr< T >
{
  return std::unique_ptr< T >( new T( std::forward< Args >( args )... ) );
}
Notice we don't have to use std::enable_if
We have a bit of a problem with arrays, because we need to know the size of the array to make.  Run-time sized arrays need the user to pass in the size, like so:
// similar to: string val[3] = { "hello", "world" }
make_unique< string[] >( 3, "hello", "world" );
// similar to: string val[1] = { "hello", "world" }
make_unique< string[] >( 1, "hello", "world" ); // throws std::bad_alloc
I'm not really fond of this because it makes the run-time sized version require an extra parameter, which mismatches with how it acts for other types.  For fixed size there's no problem:
// similar to: string val[3] = { "hello", "world" }
make_unique< string[3] >( "hello", "world" );
// similar to: string val[1] = { "hello", "world" }
make_unique< string[1] >( "hello", "world" ); // throws std::bad_alloc
// similar to: string val[] = { "hello", "world" }
make_unique< string[] >( "hello", "world" ); // allow auto sizing, implicit size
It looks like you would expect, so I propose only allowing fixed size arrays with make_unique, and add a new function just for run-time sized arrays called make_unique_array, similar to the first version of make_unique with run-time sized arrays.
template< typename T, typename S, typename... Args >
auto make_unique_array(S size, Args&&... args ) -> std::unique_ptr< T[] >
{
  static_assert( !std::is_array< T >::value, "T cannot be an array" );
  return std::unique_ptr< T[] >
      ( new T[ size ]{ std::forward< Args >( args )... } );
}
We don't allow arrays of arrays because there is no way to forward the actual initializer list into the call site of new.  Equally, we cannot forward a list of arrays and have new use it as expected.  I'll get to multirank arrays next time.  So now we have make_unique support for flat arrays.
template< typename T, typename... Args,
          typename U = typename std::remove_extent< T >::type,
          typename = typename std::enable_if<
            std::is_array< T >::value >::type >
auto make_unique( Args&&... args ) ->  std::unique_ptr< U[] >
{
  constexpr auto size = std::extent< T >::value;

  return make_unique_array< U >
      ( size ? size : sizeof...( Args ), std::forward< Args >( args )... );
}
It's a shame the language doesn't allow complete transparent argument forwarding, but maybe it will in the future, C++2x?.


Part 3 - The (updated) End (maybe?)

I found a proposal for the next C++ revision, and a discussion thread.  So I made some changes. Here's a new interface, one that I proposed.
make_unique< T >( default_init )      // new T
make_unique< T >( args... )           // new T( args... )
make_unique_from_list< T >( args... ) // new T{ args... }; got a better name?
make_unique< T[ N ] >( default_init ) // new T[ N ]
make_unique< T[ N ] >( args... )      // new T[ N ]{ args... }
make_unique< T[] >( args... )         // new T[ sizeof...( args ) ]{ args... }
make_unique_array< T >( n, default_init )    // new T[ n ]
make_unique_array< T >( n, args... )         // new T[ n ]{ args... }
make_unique_array< T >( auto_size, args... ) // new T[ sizeof...( args ) ]{ args... }
I actually think this is pretty close to what I want, but make_unique, just like make_shared, feels like it really should only be for single objects.  I'm beginning to lean towards creating a pair of new classes just for arrays, unique_array and shared_array, similar to std::array.  I want unique_array to have same overhead as a native array, exactly one pointer, but with the functionality of a container.  For shared_array, I'd like to imbed the array in the control block to reduce the number of indirections, just like make_shared does.  So look forward to that in the future.

The old code, which I put under the most liberal license I could find, is here for the taking.  Use it however you want, but don't sue me when your space station crashes to earth...
// Copyright 2013 Paul A. Tessier
//
// Licensed under the Open Source Initiative - MIT License;
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://opensource.org/licenses/mit-license.php
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#if __cplusplus != 201103L
#error requires C++11 support
#endif

#include <memory>

struct default_init_t { } default_init;

struct auto_size_t { } auto_size;

template< typename T, typename ...Args,
          typename = typename std::enable_if<
            !std::is_array< T >::value >::type >
auto make_unique( Args&& ... args ) -> std::unique_ptr< T >
{
  return std::unique_ptr< T >( new T( std::forward< Args >( args )... ) );
}

template< typename T, typename ...Args,
          typename = typename std::enable_if<
            !std::is_array< T >::value >::type >
auto make_unique( default_init_t ) -> std::unique_ptr< T >
{
  return std::unique_ptr< T >( new T );
}

template< typename T, typename ...Args,
          typename = typename std::enable_if<
            !std::is_array< T >::value >::type >
auto make_unique_from_list( Args&& ... args ) -> std::unique_ptr< T >
{
  return std::unique_ptr< T >( new T{ std::forward< Args >( args )... } );
}

template< typename T, typename... Args >
auto make_unique_array(std::size_t size, Args&&... args )
-> std::unique_ptr< T[] >
{
  static_assert(!std::is_array< T >::value || !sizeof...( Args ),
                "cannot initialize an array of arrays");
  return std::unique_ptr< T[] >
      ( new T[ size ]{ std::forward< Args >( args )... } );
}

template< typename T, typename... Args >
auto make_unique_array(auto_size_t, Args&&... args )-> std::unique_ptr< T[] >
{
  return make_unique_array< T >
      ( sizeof...( Args ), std::forward< Args >( args )... );
}

template< typename T, typename... Args >
auto make_unique_array(std::size_t size, default_init_t )
-> std::unique_ptr< T[] >
{
  return std::unique_ptr< T[] >( new T[ size ] );
}

template< typename T, typename... Args,
          typename U = typename std::remove_extent< T >::type,
          typename = typename std::enable_if< std::is_array< T >::value >::type
          >
auto make_unique( Args&&... args ) ->  std::unique_ptr< U[] >
{
  constexpr auto size = std::extent< T >::value;

  return make_unique_array< U >
      ( size ? size : sizeof...( Args ), std::forward< Args >( args )... );
}

No comments:

Post a Comment