This article is inspired from Item 30 of Effective Modern C++ by Scott Meyers.
Here we talk about the circumstances in which perfect forwarding fails.
Forwarding functions by nature are generic functions i.e template functions. To make it more generic, many forwarding functions are variadic template functions.
Template parameters not only encode Lvalue, Rvalueness, but also other important characteristics like const and volatile.
.
Since perfect forwarding is pervasive in many of the C++11 features, like std::make_unique etc,we should be aware of the scenarios when forwarding might fail. These cases are listed below.
For illustration let's assume we have functions
forwardingFunc calls func by forwarding it's parameters.
Braced initializers
Suppose our function is
void func(std::vector<int>& params)
and we call this the function func directly using func({1,2,3,4}); it compiles fine.
But if we call the function forwardingFunc like this forwardingFunc ({1,2,3,4}); , it does not compile.
This is because if we call func directly, the compiler will try to cast it to the right type and thus creates a temporary vector.
But if we call forwardFunc, and since forwardingFunc is a template function, compiler tries to deduce the parameters passed.
In this case, compilers are not able to deduce the type of {1,2,3,4}.
However if we were to do
auto list = {1,2,3,4}; // according to item 2, auto deduces this to std::initializer_list<int>
forwardingFunc(list); // this compiles fine.
Using 0 or NULL
Lets say our function is
void func(int* ptr)
In one of items covered in the book, we are asked to use nullptr instead of 0 or NULL to denote null pointers. Here is one reason to do so. When 0 or NULL is passed, the template deduces it to an int instead of null pointer. and when a int is forwarded to func (which takes int pointer and not an int), it produces compiler error.
Declaration of integral static const data members
Note that this is for only const static integral data members. Not for other data types.
Compilers usually does const propagation for such data members, and wherever it is used, it is substituted with the value (similar to macro, 23 in the below example).
Hence, the compiler does not create a memory location for these kind of variables. But the compiler does need a memory to be allocated, if we were to point to it using a pointer. And as we know, reference is also internally implemented as pointer ( automatically de-referenced pointer). In our case, we are using a reference, universal reference that is. This causes link time issues in some compilers.
But if we make a small change as shown below, the code works fine in Ubuntu as well.
If we define the static member variable, memory gets allocated to it and the link time error disappears. We can also note that, this behavior is not consistent across all compilers.But for the code to be portable across other compilers and systems, it is better to define the variable.
There are two other cases - overloaded function names and template names. and bitfields, which we will deal in a separate article
Here we talk about the circumstances in which perfect forwarding fails.
Forwarding functions by nature are generic functions i.e template functions. To make it more generic, many forwarding functions are variadic template functions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
template<typename... Ts> | |
void func(Ts&&... params) | |
{ | |
funcB(std::forward<Ts>(params)...); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "boost/type_index.hpp" | |
#include <iostream> | |
template<typename T> | |
void func(T&& obj) // universal reference | |
{ | |
using boost::typeindex::type_id_with_cvr; | |
std::cout << "obj = " << obj << '\n'; | |
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << '\n'; | |
} | |
int main() | |
{ | |
const int x = 2; | |
volatile float y = 3.; | |
func(x); | |
func(y); | |
} | |
/*Output | |
obj = 2 | |
T = int const& | |
obj = 3 | |
T = float volatile& | |
*/ |
For illustration let's assume we have functions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
void func(params); | |
//template function which calls func by forwarding it's parameters. | |
void forwardingFunc (params); |
forwardingFunc calls func by forwarding it's parameters.
Braced initializers
Suppose our function is
void func(std::vector<int>& params)
and we call this the function func directly using func({1,2,3,4}); it compiles fine.
But if we call the function forwardingFunc like this forwardingFunc ({1,2,3,4}); , it does not compile.
This is because if we call func directly, the compiler will try to cast it to the right type and thus creates a temporary vector.
But if we call forwardFunc, and since forwardingFunc is a template function, compiler tries to deduce the parameters passed.
In this case, compilers are not able to deduce the type of {1,2,3,4}.
However if we were to do
auto list = {1,2,3,4}; // according to item 2, auto deduces this to std::initializer_list<int>
forwardingFunc(list); // this compiles fine.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <iostream> | |
#include <vector> | |
void func(const std::vector<int>& vc) | |
{ | |
for (auto x : vc) | |
{ | |
std::cout << x; | |
} | |
std::cout << '\n'; | |
} | |
template<typename T> | |
void forwardingFunc(T&& param) | |
{ | |
func(std::forward<T>(param)); | |
} | |
int main() | |
{ | |
func({ 1,2,3,4 }); // compiles fine. | |
auto ini_list = { 1,2,3,4 }; | |
forwardingFunc(ini_list); //compiles fine | |
//forwardingFunc({ 1,2,3,4 }); // does not compile if uncommented | |
//compiler errors | |
// 1>c:\users\*****\mainfiilecpp.cpp(56): error C2672: 'forwardingFunc': no matching overloaded function found | |
// 1>c:\users\*****\mainfiilecpp.cpp(56) : error C2783 : 'void forwardingFunc(T &&)' : could not deduce template argument for 'T'// | |
} |
Using 0 or NULL
Lets say our function is
void func(int* ptr)
In one of items covered in the book, we are asked to use nullptr instead of 0 or NULL to denote null pointers. Here is one reason to do so. When 0 or NULL is passed, the template deduces it to an int instead of null pointer. and when a int is forwarded to func (which takes int pointer and not an int), it produces compiler error.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <iostream> | |
#include <vector> | |
void func(int* x) | |
{ | |
if (x != nullptr) | |
std::cout << *x; | |
} | |
template<typename T> | |
void forwardingFunc(T&& param) | |
{ | |
func(std::forward<T>(param)); | |
} | |
int main() | |
{ | |
func(0); // works but not recommended | |
func(nullptr); // right way to call func directly | |
//forwardingFunc(NULL); // does not compile. Gives the below error | |
//1>c:\***\mainfiilecpp.cpp(13) : error C2664 : 'void func(int *)' : cannot convert argument 1 from 'int' to 'int *' | |
forwardingFunc(nullptr); //compiles fine | |
} |
Declaration of integral static const data members
Note that this is for only const static integral data members. Not for other data types.
Compilers usually does const propagation for such data members, and wherever it is used, it is substituted with the value (similar to macro, 23 in the below example).
Hence, the compiler does not create a memory location for these kind of variables. But the compiler does need a memory to be allocated, if we were to point to it using a pointer. And as we know, reference is also internally implemented as pointer ( automatically de-referenced pointer). In our case, we are using a reference, universal reference that is. This causes link time issues in some compilers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//classA.h | |
struct A | |
{ | |
static const int var = 23; // this is declaraion only | |
}; | |
//MainFile.cpp | |
#include <iostream> | |
#include "classA.h" | |
void func(int x) | |
{ | |
std::cout << x << '\n'; | |
} | |
template<typename T> | |
void forwardingFunc(T&& var) | |
{ | |
func(std::forward<T>(var)); | |
} | |
int main() | |
{ | |
func(A::var); //works | |
forwardingFunc(A::var); // works in visual studio, link time error in Gcc 5.4 in Ubuntu | |
} |
But if we make a small change as shown below, the code works fine in Ubuntu as well.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//classA.h | |
struct A | |
{ | |
static const int var; // this is declaraion only | |
}; | |
const int A::var = 23; | |
//MainFile.cpp | |
#include <iostream> | |
#include "classA.h" | |
void func(int x) | |
{ | |
std::cout << x << '\n'; | |
} | |
template<typename T> | |
void forwardingFunc(T&& var) | |
{ | |
func(std::forward<T>(var)); | |
} | |
int main() | |
{ | |
func(A::var); //works | |
forwardingFunc(A::var); // works in visual studio and in Gcc 5.4 in Ubuntu | |
} |
If we define the static member variable, memory gets allocated to it and the link time error disappears. We can also note that, this behavior is not consistent across all compilers.But for the code to be portable across other compilers and systems, it is better to define the variable.
There are two other cases - overloaded function names and template names. and bitfields, which we will deal in a separate article
Modern (Effective) C++ - Perfect forwarding fail cases
Reviewed by zeroingTheDot
on
February 28, 2018
Rating:

No comments: