Variable-length parameters in C + + deep understanding of _c language

Source: Internet
Author: User
Tags function definition numeric value

Objective

In a project sucked in in order to use shared memory and custom memory pools, we define functions ourselves and MemNew automate constructors for non pod types within functions. Call the custom function where you want it MemNew . This creates a problem where the class using the STL has a default constructor, as well as a copy constructor, and so on. But classes that use shared memory and memory pools may not have a default constructor, but rather a constructor that defines more than one parameter, and then how to pass the arguments in to the MemNew function becomes a problem.

Variable-length parameter function

First of all, the more used variable length parameter function, the most classic is printf .

extern int printf (const char *format, ...);

The above is a function declaration of a variable length parameter. We define a test function ourselves:

#include <stdarg.h>
#include <stdio.h>

int testparams (int count, ...)
{
 va_list args;
 Va_start (args, count);
 for (int i = 0; i < count; ++i)
 {
  int arg = va_arg (args, int);
  printf ("Arg%d =%d", I, Arg);
 }
 Va_end (args);
 return 0;
}

int main ()
{
 testparams (3, one, one);
 return 0;
}

The resolution of the variable length parameter function is used to three macros va_start , va_arg and va_end , again, va_list the definition typedef char* va_list; is just a char pointer.

How do these macros parse incoming arguments?

Function of the call is a press stack, save, jump process.

The simple process description is as follows:

1, the parameters from right to left in turn into the stack;

2, call the call instruction, the next order to execute the address of the instruction as a return address into the stack; (the called function will go back to that address after execution)

3, the current EBP (base address pointer) into the stack to save, and then the current ESP (stack top pointer) assigned to EBP as the base of the new function stack frame;

4, execute the CALL function, local variables, etc. into the stack;

5, the return value into the EAX,LEAVE,EBP assigned to the ESP,ESP address assigned to EBP; (You may need to copy the temporary return object here)
To proceed from the return address (the address of the return address to the EIP)

Since the beginning of the time from right to left the parameters of the stack, passed to the va_start leftmost parameters, the right parameters are pressed into the stack earlier, so the address is incremented (the top address is the smallest). Passing in the type of the parameter that you va_arg want to get, you can take advantage of the sizeof calculated offset and then get the following parameter values.

#define _INTSIZEOF (n)   (sizeof (n) + sizeof (int)-1) & ~ (sizeof (int)-1))

#define _ADDRESSOF (v) (&const_ Cast<char&> (reinterpret_cast<const volatile char&> (v)))

#define __CRT_VA_START_A (AP, V) ( void) (AP = (va_list) _addressof (v) + _intsizeof (v))
#define __CRT_VA_ARG (AP, T)  (* (t*) (AP + + _intsizeof (t))-_ Intsizeof (t))
#define __CRT_VA_END (AP)  ((void) (AP = (va_list) 0))

#define __CRT_VA_START (AP, x) ((void) ( __vcrt_va_start_verify_argument_type<decltype (x) > (), __crt_va_start_a (AP, x))

#define Va_start __crt_va _start
#define VA_ARG __crt_va_arg
#define Va_end __crt_va_end

In the macro definition above, the _INTSIZEOF(n) lower 2-bit instruction of the address is aligned to 4 bytes of memory. Each time a parameter is taken, the call __crt_va_arg(ap,t)  returns the value of the T-type parameter address, and the AP is offset to T. Finally, the call points _crt_va_end(ap) the AP to 0.

The use and principle of the function of variable length parameter it is very understandable to see the macro definition. As we know from the foregoing, to use the parameters of a variable-length parameter function, we must be aware of the type of each parameter passed in. printf, there is a format combination of special characters in the string to resolve the subsequent parameter types. But when you pass in the parameters of the constructor of the class, we don't know what each parameter is, although the arguments can pass in the function, but cannot parse and get the numeric value of each parameter. Therefore, the traditional variable length parameter function is not sufficient to solve the problem of passing in arbitrary constructor parameters.

Second, variable length parameter template

We need to use the new features of c++11, variable length parameter template.

Here's an example of using a custom memory pool. Defines a memory pool class MemPool.h that count allocates memory for the cell with a type T, and assigns an object by default. When there is not enough free memory in memory, request MEMPOOL_NEW_SIZE a memory object at once. The memory pool itself is responsible for memory allocation only, does not do initialization work, therefore does not need to pass in any parameter, only then instantiates the template to allocate the corresponding type of memory.

#ifndef util_mempool_h #define UTIL_MEMPOOL_H #include <stdlib.h> #define MEMPOOL_NEW_SIZE 8 Template<typenam
  E T, size_t count = 1> class Mempool {private:union memobj {char _obj[1];
 memobj* _freelink;

};
  Public:static void* Allocate () {if (!_freelist) {refill ();
  } memobj* alloc_mem = _freelist;
  _freelist = _freelist->_freelink;
  ++_size;
 Return (void*) Alloc_mem;
  } static void Deallocate (void* p) {memobj* q = (memobj*) p;
  Q->_freelink = _freelist;
  _freelist = q;
 --_size;
 Static size_t TotalSize () {return _totalsize;
 static size_t size () {return _size;
  } private:static void Refill () {size_t size = sizeof (t) * count;
  char* New_mem = (char*) malloc (size * mempool_new_size);
   for (int i = 0; i < mempool_new_size ++i) {memobj* Free_mem = (memobj*) (New_mem + i * SIZE);
   Free_mem->_freelink = _freelist;
  _freelist = Free_mem;
 } _totalsize + = Mempool_new_size; } Static memobj* _freelisT
 Static size_t _totalsize;
Static size_t _size;

}; Template<typename T, size_t count> TypeName mempool<t, count>::memobj* mempool<t, count>::_freelist =

NULL;

Template<typename T, size_t count> size_t mempool<t, count>::_totalsize = 0;
Template<typename T, size_t count> size_t mempool<t, count>::_size = 0; #endif

Next, in the absence of variable length parameters, the implementation of common MemNew and MemDelete function templates. This does not explain the function template in detail, with function templates we can implement the same memory pool allocation for different types. As follows:

Template<class t>
T *memnew (size_t count)
{
 t *p = (t*) mempool<t, Count>::allocate ();
 if (P!= NULL)
 {
  if (!std::is_pod<t>::value)
  {for
   (size_t i = 0; i < count; ++i)
   {
    NE W (&p[i]) T ();
 }} return p;
}

Template<class t>
t *memdelete (t *p, size_t count)
{
 if (P!= NULL)
 {
  if (!std::is_pod <t>::value)
  {for
   (size_t i = 0; i < count; ++i)
   {
    p[i].~t ();
   }
  }
  Mempool<t, Count>::D eallocate (P);
 }

In the above implementation, a default constructor is used to construct the requested memory, and an error is made placement new when the requested memory type does not have a default constructor placement new . For pod types, you can omit the process of calling a constructor.

After the introduction of c++11 variable length template parameters memnew modified as follows

Template<class T, class ... Args>
T *memnew (size_t count, Args&& ... Args)
{
 T *p = (t*) mempool<t, Count>::allocate ();
 if (P!= NULL)
 {
  if (!std::is_pod<t>::value)
  {for
   (size_t i = 0; i < count; ++i)
   {
    new (&p[i]) T (std: :forward<args> (Args).);}
 return p;
}

The function definition above contains a number of attributes, which I'll explain one at a time, which class... Args represents the variable-length parameter template, which is args&& to a right value reference in the function argument. std::forward<Args> implement the perfect forwarding of parameters. In this way, no matter what constructors the incoming type has, it is perfectly possible to execute

In c++11, the concept of variable length parameter template is introduced to solve the template with uncertain parameter number.

Template<class ... T> class Test {};
Test<> test0;
Test<int> test1;
Test<int,int> test2;
Test<int,int,long> test3;

Template<class ... t> void Test (T ... args);
Test ();
test<int> (0);
Test<int,int,long> (0,0,0l);

The above are examples of using variable-length parameter class templates and variable-length parameter function templates.

2.1 Variable length parameter function template

T... argsAs the formal parameter package, where args is a pattern, and the formal parameter package can have 0 to any number of parameters. When you call a function, you can pass any number of arguments. How do you use a parameter bundle for a function definition? In the previous section MemNew , we used the std::forward parameter bundle in sequence to pass in the constructor, without paying attention to what each parameter was. If necessary, we can use the sizeof...(args) operation to get the number of parameters, but also can expand the parameter package, for each parameter to do more. There are two types of expanded methods, recursive functions, and comma expressions.

The recursive function mode expands, when the template is deduced, the layer is recursively expanded, and finally terminates with the defined general function when there is no parameter.

void Test ()
{
}

template<class T, class ... args> 
void Test (T-Args. Args)
{
 std::cout << typeid (t). Name () << "" << firs T << Std::endl;
 Test (args ...);
}

Test<int, int, long> (0, 0, 0L);

Output:
int 0
int 0
Long 0

The comma expression expands to execute each argument, using the array's arguments to initialize the list and the comma expression print .

Template<class t>
void print (t arg)
{
 std::cout << typeid (t). Name () << "" << Arg ;< Std::endl;
}

Template<class ... args>
void Test (Args ... Args)
{
 int arr[] = {(Print (Args), 0) ...

} Test (0, 0, 0L);

Output:
int 0
int 0
Long 0

2.2 Variable length parameter class template

Variable-length parameter class template, under normal circumstances can be convenient for us to do some compile-time calculation. The template parameters can be expanded sequentially by the way of skewness and recursive inference.

Template<class T, class ... Types>
class Test
{public
:
 enum {
  value = Test<t>::value + test<types...>:: Value,
 };
};

Template<class t>
class test<t>
{public
:
 enum {
  value = sizeof (T),
 };
};

test<int, int, long> Test;
Std::cout << Test.value;

Output:12

2.3 Right value reference and perfect forwarding

For variable-length parameter function templates, the need to expand the parameters of the formal parameter package is not much, or more like the requirements of this article MemNew , eventually the whole pass into an existing function. We focus on the transfer of parameters.

To understand the right value reference, you need to first clarify the left and right values. The left value is the value of an expression in memory that has an object that determines which address is stored, and the value of an expression that has a right value that is a non-left value. The const left value cannot be assigned, and the right value of the temporary object can be assigned. The fundamental difference between the left and right values is whether the memory address can be obtained using the & operator.

int i =0;//i left value
int *p = &i;//I left value
int& foo ();
Foo () = 42;//foo () left value
int* p1 = &foo ();//foo () left value

int foo1 ();
int j = 0;
j = foo1 ()/Foo right value
int k = j + 1;//J + 1 Right value
int *p2 = &FOO1 ();//error, address
j = 1;//1 Right value cannot be fetched

After you understand the left and right values, and then look at the reference, the reference to the left value is a reference to the left value, the right value (pure right value and deathbed value) refers to the right value reference.

The following function foo , incoming int type, return int type, where parameter 0 and return value 0 of the passed function are all the right values (cannot use the & to get the address). So, without optimization, when we pass in parameter 0, we need to copy the right value 0 param , and the function returns with a copy of 0 to the temporary object, and the temporary object to Res. Of course now the compiler has done the return value optimization, the return object is directly created on the return of the left value, here only for example

int foo (int param)
{
 printf ("%d", param);
 return 0;
}

int res = foo (0);

Obviously, the copies here are redundant. Perhaps we would like to optimize, first of all, to change the parameter int int& , passed the left value reference, so 0 can not be introduced, of course, we could change const int& , so that finally save the copy of the reference.

int foo (const int& param)
{
 printf ("%d", param);
 return 0;
}

Because it can be either const int& a left or a right, incoming 0 or int variables can be satisfied. (but it seems that since there is a type with a left-value reference int& , there should be a corresponding type of reference to the right value int&& ). In addition, the right value returned here is 0, and it does not seem to be possible to assign a value to the left value without copying it res .

So with the move semantics, the contents of the temporary object are moved directly to the assigned value of the left object ( std::move ). And right value reference,x&& is a reference to the right value of the data type X.

int result = 0;
int&& foo (int&& param)
{
 printf ("%d", param);
 return Std::move (result);
}

int&& res = foo (0);
int *pres = &res;

The foo right value reference parameter and the return value are changed, and the right value reference is returned, eliminating the copy. Here res is a named reference, and the right value reference to the right of the operator as the left value can be addressed. The right value refers to both the left-valued property and the right-value property.

The above example also exists only in the performance issue of the copy. Back to MemNew such a function template.

Template<class t>
t* Test (t arg)
{return
 new T (ARG);
}

Template<class t>
t* Test (t& Arg)
{return
 new T (ARG);
}

Template<class t>
t* Test (const t& arg)
{return
 new T (ARG);

Template<class t>
t* Test (t&& Arg)
{return
 new T (Std::forward<t> (ARG));
}

The first of the above three ways to pass the parameter, firstly there is a copy consumption, and then some of the parameters are required to modify the left value. The second way is to not pass the constant and other right values. The third way, although the right value of the left value can be transmitted, cannot modify the parameters passed in. The fourth way to use the right value reference, you can solve the problem of perfect parameter forwarding.

Std::forward is able to pass the parameter intact by returning the corresponding type's left and right value reference according to the data type of the argument.

  To explain this principle involves citing collapsing rules

t& &->t&

t& &&->T&

t&& &->T&

t&& &&->T&&

template< class T > struct remove_reference  {typedef t type;};
template< class T > struct remove_reference<t&> {typedef t type;};
template< class T > struct remove_reference<t&&> {typedef t type;};

template< class T > t&& forward (TypeName std::remove_reference<t>::type& T)
{
 return Static_cast<t&&> (T);
}

Template<class t>
TypeName std::remove_reference<t>::type&& Move (t&& a) noexcept
{return 
 static_cast<typename std::remove_reference<t>::type&&> (a);
}

for function templates

Template<class t>
t* Test (t&& Arg)
{return
 new T (Std::forward<t> (ARG));
}

When the incoming argument is an X-type left value, T is X&, and the last type is x&. When the argument is the right value of type X, T is x, and the last type is x&&.

When x is the left value:

x x;
Test (x);

T is X&, after instantiation

x& && Std::forward (remove_reference<x&>::type& a) noexcept
{return
 static_cast <X& &&> (a);
}

x* Test (x& && Arg)
{return
 new X (Std::forward<x&> (ARG)); 
}

After the collapse

x& Std::forward (x& a)
{return
 static_cast<x&> (a);
}

x* Test (x& Arg)
{return
 new X (Std::forward<x&> (ARG));
}

When x is the right value:

X foo ();
Test (foo ());

T is x, after instantiation

x&& Std::forward (remove_reference<x>::type& a) noexcept
{return
 static_cast<x& &> (a);
}

x* Test (x&& Arg)
{return
 new X (Std::forward<x> (ARG)); 
}

After the collapse

x&& Std::forward (x& a)
{return
 static_cast<x&&> (a);
}

x* Test (x&& Arg)
{return
 new X (Std::forward<x> (ARG));
}

You can see that the final argument is always inferred to be the same type reference as it was passed in.

So far, we discuss the variable length parameter template, discuss the perfect forwarding of the right value reference and function template, and fully explain the MemNew parameter transfer process of the constructor of any multiple parameters. Using variable length parameter function template, right value reference and std::forward , can complete the parameter of perfect forwarding.

Summarize

The above is the entire content of this article, I hope the content of this article for everyone to learn or use C + + can help, if there is doubt you can message exchange, thank you for the cloud Habitat Community support.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.