Avoid creating patterns for different char array sizes

I have a simple template variable code to write an argument to a stream:

#include <iostream> void tostream(std::ostream& os) { } template<typename T, typename... Args> void tostream(std::ostream& os, const T& v, const Args&... args) { os << v; tostream(os, args...); } template<typename... Args> void log(std::ostream& os, const Args&... args) { tostream(os, args...); } 

with whom I can call:

 log(std::cout, "Hello", 3); log(std::cout, "Goodbye", 4); 

I will compile this code with Visual Studio 2013 with all optimizations (Release config) and open the resulting executable using the IDA disassembler.
I see that the compiler created two copies of the log() function. The one that takes const char[6], int and the one that takes const char[8], int .
It also appears in the debugger when entering these functions and viewing the call stack window.
Two functions are identical except for their signature.

Is there any way to convince the compiler that these two functions should actually be one function that takes const char*, int , and not two functions?

This is a problem for me, as I have hundreds of these features inflating the size of my executable, most of which could have been avoided.
There would still be many instances of the function for different combinations of arguments, but there would be much fewer, since I only have very few possible combinations of arguments.

One thing that would allow this is to call the function as follows:

 log(cout, (const char*)"Hello", 3); log(cout, (const char*)"Goodbye", 4); 

But this is unacceptable, since it is very cluttered with code.

+6
source share
2 answers
  template<class T> using decay_t = typename std::decay<T>::type; template<typename... Args> void log(ostream& os, const Args&... args) { tostream(os, decay_t<Args>(args)...); } 

will manually decompose your arguments before passing them tostream . This converts functions to function pointers, array references to pointers, etc.

This may cause some false copies. For primitive types, there is no problem, but wasteful for std::string and the like. So, a narrower solution:

 template<class T> struct array_to_ptr { using type=T; }; template<class T, size_t N> struct array_to_ptr<T(&)[N]> { using type=T*; }; template<class T> using array_to_ptr_t=typename array_to_ptr<T>::type; template<typename... Args> void log(ostream& os, const Args&... args) { tostream(os, array_to_ptr_t<Args const&>(args)...); } 

which will do this only for arrays.

Note that various log implementations may still exist, but not tostream . Clear log implementations should be eliminated by folding and / or inlining comdat log , and possibly eliminating the recursion requirement (to notice that it is bent) will make it easier.

Finally, this may be useful:

 template<typename... Args> void tostream(std::ostream& os, const Args&... args) { using expand=int[]; (void)expand{0, ((os << args),void(),0)... }; } 

which performs direct expansion without recursion on arguments in one function. Your compiler should be smart enough to understand that an implied array of 0 useless, and even if it's not so much overhead compared to io.

+6
source

I don’t have the right solution for you, but in terms of workarounds, will the following be less β€œmessy” for you?

 log(cout, +"Hello", 3); log(cout, +"Goodbye", 4); 

I appreciate that you still need your users to remember that sucks.

+1
source

Source: https://habr.com/ru/post/984630/


All Articles