Tags

,

It is natural to believe that when we convert a code to template, it must operate similarly. For example, some “int Max(int a, int b)” function, originally written for int, will work well with long, double etc. after conversion to template.

However, a simple replacement of types with template parameters does not always result in code that works analogously.

The next func functions accept either a constant reference (to deal with unmodifiable values) or an rvalue reference (for moveable and temporary values). Such pairs of functions are especially common for large structures; the first will copy, the second will move data.

For simplicity, let’s experiment with int:

void func( const int & a )
{
    . . . .
}

void func( int && a )
{
    . . . .
}

 
After direct conversion to templates, the code will work differently:

Before After
//
void func( const int & a )
{
    cout << "1" << endl;
}


void func( int && a )
{
    cout << "2" << endl;
}

void main()
{
    int i = 100;
    func( i ); // prints ‘1’
}
template<typename T>
void func( const T & a )
{
    cout << "1" << endl;
}

template<typename T>
void func( T && a )
{
    cout << "2" << endl;
}

void main()
{
    int i = 100;
    func( i ); // prints ‘2’
}

So, it outputs “1” in original version, but “2” in template version.

Therefore, our assumption of similarity is inappropriate here. The behaviour is caused by the specific argument deduction in case of templates [1, § 14.8.2.1].

The type int && is an rvalue reference, while T && is a “universal reference” [2, 3]. Such universal references bind to anything. To investigate this, let’s do a short experiment:

template<typename T>
void test( T && )
{
    cout << __FUNCSIG__ << endl;
}

void main()
{
    int i = 100;
    const int ci = i;
    int & ir = i;
    const int & cir = ci;

    test( i );          // Prints "test<int&>( int & )"
    test( ci );         // Prints "test<const int&>( const int & )"
    test( ir );         // Prints "test<int&>( int & )"
    test( cir );        // Prints "test<const int&>( const int & )"
    test( move( i ) );  // Prints "test<int>( int && )"
    test( move( ci ) ); // Prints "test<const int>( const int && )"
}

 
We see that all of the arguments are accepted. The __FUNCSIG__ macro shows how compiler deduces the types.

Therefore, while ‘&&’ is frequently identified as a temporary or moveable value in non-template context, it has a different meaning and coverage in case of template type deduction.

How to force the template version of our pair of func functions to work similarly to original non-template version?

In initial implementation, the type of data was a simple int, then we added “decorations”, such as const, & and &&. Let’s follow the same way and indicate that T denotes the most “inferior” type, without const, & and &&. To achieve this, the standard library offers a utility class: decay [4].

To isolate our template functions, where T will be a decayed type, let’s put them inside a namespace:

namespace Internal
{
    template<typename T>
    void func( const T & a )
    {
        cout << "1" << endl;
    }

    template<typename T>
    void func( T && a )
    {
        cout << "2" << endl;
    }
}

Then we write an intermediate “dispatcher” function, which will select one of the above according to received argument. With decay, we will determine the inferior T type. Then we will direct the argument to one of the functions according to its original type — an action which is called “perfect forwarding” and is achieved using forward [5]. Thus, our dispatching function is:

template<typename T>
void func( T && a )
{
    Internal::func<decay<T>::type>( forward<T>( a ) );
}

Now we achieved a template version that works similarly to original variant. Let’s do some experiments:

Before After
//


void func( const int & a )
{
	cout << "1" << endl;
}


void func( int && a )
{
	cout << "2" << endl;
}









void main()
{
	int i = 100;
	const int ci = i;
	int & ir = i;
	const int & cir = ci;

	func( i ); // prints ‘1’
	func( ci ); // prints ‘1’
	func( ir ); // prints ‘1’
	func( cir ); // prints ‘1’
	func( move( i ) ); // prints ‘2’
	func( move( ci ) ); // prints ‘1’
	func( 200 ); // prints ‘2’
}
namespace Internal
{
	template<typename T>
	void func( const T & a )
	{
		cout << "1" << endl;
	}

	template<typename T>
	void func( T && a )
	{
		cout << "2" << endl;
	}
}

template<typename T>
void func( T && a )
{
	Internal::func<decay<T>::type>( forward<T>( a ) );
}

void main()
{
	int i = 100;
	const int ci = i;
	int & ir = i;
	const int & cir = ci;

	func( i ); // prints ‘1’
	func( ci ); // prints ‘1’
	func( ir ); // prints ‘1’
	func( cir ); // prints ‘1’
	func( move( i ) ); // prints ‘2’
	func( move( ci ) ); // prints ‘1’
	func( 200 ); // prints ‘2’
}

 

Conclusions

Adding “template” keywords does not always result in code that works similarly to original implementation — primarily in case of “&&”.

“Sadly, this is not some esoteric problem you can safely forget about. I’ve seen people make this mistake in the real world, and in one case, the code was accidentally moving from an lvalue as a result, leaving a ticking time bomb in production code” [3].

The solution considered here is based on decay and forward. There are different approaches, such as one based on additional fake parameter and is_lvalue_reference [3], which requires some more adjustments to original functions. Some other approaches uses enable_if and remove_reference.

References

[1] Working Draft, Standard for Programming Language C++http://open-std.org/JTC1/SC22/WG21/docs/papers/2011/n3242.pdf

[2] Scott Meyers. Universal References in C++11http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers.

[3] Eric Niebler. Universal References and the Copy Constructorhttp://ericniebler.com/2013/08/07/universal-references-and-the-copy-constructo/.

[4] MSDN. decay Classhttps://msdn.microsoft.com/en-us/library/ee361638.aspx.

[5] MSDN. forwardhttps://msdn.microsoft.com/en-us/library/ee390914.aspx.


Advertisements