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 ]- complete type
- cannot be copied or assigned to directly
- decays to a pointer of the sub-type
- addressable (can pass by reference or address)
// 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[]- incomplete type (only useful in a template parameter)
- decays to a pointer of the sub-type
- ???
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_ifWe 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