C++ advanced(4)make function and SFINAE

 This article is not suitable for beginners

目录

make function

allocate_shared

make_unique &&make_shared  

Advantage

limit

Parameter function of custom deleter

Pimpl

SFINAE mechanism

explain

introducing

viod_t (C++ 17)


make function

There are three make functions in C++. The most commonly used one is make_shared. In addition, there are make_unique and allocate_shared proposed by C++14. 

allocate_shared

Except that the first parameter is an allocator object used to dynamically allocate memory, it behaves like std::make_shared. Of course, there are some differences that make_shared supports array types (C++17), but allocate_shared does not support, this function is in boost implemented in the library.

This function is very simple, put a simple example:

// allocate_shared example
#include <iostream>
#include <memory>

int main () {
  std::allocator<int> alloc;    // the default allocator for int
  std::default_delete<int> del; // the default deleter for int

  std::shared_ptr<int> foo = std::allocate_shared<int> (alloc,10);

  auto bar = std::allocate_shared<int> (alloc,20);

  auto baz = std::allocate_shared<std::pair<int,int>> (alloc,30,40);

  std::cout << "*foo: " << *foo << '\n';
  std::cout << "*bar: " << *bar << '\n';
  std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';

  return 0;
}

result:

*foo: 10
*bar: 20
*baz: 30 40

make_unique &&make_shared  

Advantage

We know that to construct smart pointers, it is generally recommended to use direct new or make, but we still hope to use make as much as possible. Because if you use new it will be like this:

std::shared_ptr<Widget> s(new Widget);//不使用make函数
std::unique_ptr<Widget> u(new Widget);//不使用make函数

In this way, we need to write the widget twice, and code duplication should be avoided. Duplication in the source code will increase the number of compilations, so it is not recommended.

The second reason is that there may be memory leaks when the function is passed. For example, we define an interface as follows:

void processWidget(std::shared_ptr<Widget> spw,int priority);

There seem to be some problems with passing value, but since it is a copy of std::shared_ptr, it is acceptable, but if we pass in an rvalue, it may cause a memory leak :

processWidget(std::shared_ptr<Widget>(new Widget),createMyInt())

Why, because before processWidget runs, it will produce the following three parts of code:

  1. new Widget
  2. cerateMyInt ()
  3. shared_ptr constructor

 The execution order of these steps cannot be determined. If the above sequence is above, there will be a problem with the execution of the second item, and an exception will be thrown. There is no way to use a smart pointer to take over new, so memory leaks may occur. If you use make instead, you can avoid this problem.

processWidget(std::make_shared<Widget>(),createMyInt())

The third reason is to improve efficiency , consider the following code:

std::shared_ptr<Widget> spw(new Widget);

Obviously only one new is needed, but everyone knows that the control block inside shared_ptr is actually new, so in fact, new is executed twice, but if you use make function only once, the object and control block are all new at one time. Instead of new in different places twice, the execution speed of the code is accelerated. It is worth noting that if new is separated, some additional record information such as debug information will be allocated, potentially reducing the memory pin number.

The efficiency analysis of std::make_shared above is also used for std::allocate_shared, so
the performance advantages of std::make_shared can also be extended to the std::allocate_shared function.

limit

However, having said that, make is not used everywhere, so as we all know, shared or unique can customize the deleter, but make cannot customize the deleter .

The second limitation comes from the construction method . Normally, a type is created in two ways, with initializer_list or not, depending on whether it is overloaded or not. The make function will perfectly forward its parameters to the object's constructor, but it uses square brackets instead of curly brackets. If we want to initialize the object with curly brackets, that is, initializer_list, we must use new to achieve perfect forwarding. Both of the following methods are wrong.

auto spv = std::make_shared<std::vector<int>>{1,2,3};
auto spv = std::make_shared<std::vector<int>>({1,2,3});

 But if we insist on using initializer_list, we can use this compromise:

//使用std::initializer_list创建
auto initList = { 10, 20 };
//使用std::initializer_list为参数的构造函数来创建std::vector
auto spv = std::make_shared<std::vector<int>>(initList);

 The above two cases are only for the restrictions of std::unique_ptr, and std::shared_ptr has two other restrictions in addition to the above two restrictions:

First of all, we know that in addition to the global operator new, we can customize new in the class. The first parameter must be size_t, and the delete function can be defined at the same time. The first parameter must be void *. This custom method is usually created by new. The size of sizeof(class) comes out of and delete. For shared_ptr, the allocated memory is not only the size of the object, but also needs to add a control block with a size of 16byte, pointing to two reference counts, deleters, and even allocator. Using make is really not a good idea for custom new and delete operations .

The fourth problem, since the make function constructs the control block and the object together, if the reference count of the control block is zeroed, the object will be released, but the control block will not be released immediately , because there are two reference counts, namely shared_count And weak_count, as long as these two things are not 0, then never want to release the entire object for a long time. But new will not have this phenomenon, because the memory allocated separately can be reclaimed separately, the control block is reserved and released later, but the object is released first.

The fifth question, if new fails, usually throw an exception directly. Many people write this:

new (std::nothrow)

 In this way, new can avoid the exception of allocating space , but will throw a null pointer. Many developers prefer this form of new for various considerations. If we create smart pointers and want to use this form, then use the constructor function It is inevitable.

Parameter function of custom deleter

Going back to the previous memory leak problem, if we want to pass a custom deleter, then we will have to call this form:

void myLog(T* t) //deleter
{
	cout << typeid(t).name() << endl;
}
auto myDel = [](Ispliter* s)
{
	myLog(s);
	delete s;
};
processWidget(std::shared_ptr<T>(new T,myDel),creatMyInt())//as before,可能会造成内存泄露

A very simple solution is as follows:

std::shared_ptr<T>s(new T,myDel)
processWidget(s,creatMyInt())

But if we use rvalue transfer, it will be much better. Compared with ordinary copy prevention, it also avoids changing the reference count. Since the reference count is of atomic type, the change will be slower.

processWidget(std::move(s),creatMyInt())

Pimpl

Another case is the Pimpl (pointer to implementation) operation, such as the common declaration of a member variable as a pointer to an incomplete type, and then dynamic allocation and recovery in the original data member object. For example this case:

#include<iostream> //*.h file
class Widget {
public:
	Widget(); //declaration only
private:
	struct Impl;
	std::unique_ptr<Impl> pImpl;//use smart pointer
};
#include "test.h" //in "*.cpp"
#include <string>
#include <vector>

struct Widget::Impl {
	std::string name;
	std::vector<double> data;
};
Widget::Widget()
	:pImpl(std::make_unique<Impl>()) 
{} 

We don't need to define the destructor here, because we use ptr to receive it. The problem is that if we compile it like this, it will report an error.

Widget w; //error

The reason is that it thinks I'm using the undefined type Impl, but the truth is we defined it, why is this happening. Let's look at the code, here we use sizeof for our type Impl, which is generated when the default destructor is generated.

 The key is that when the widget's destructor is generated, it is necessary to ensure that the definition of Impl is seen by the compiler, so we need to define the destructor after defining the function of Impl to avoid the above error situation:

#include<iostream> //*.h file
class Widget {
public:
	Widget(); //declaration only
    ~Widget();
private:
	struct Impl;
	std::unique_ptr<Impl> pImpl;//use smart pointer
};
#include "test.h" //in "*.cpp"
#include <string>
#include <vector>

struct Widget::Impl {
	std::string name;
	std::vector<double> data;
};
Widget::Widget()
	:pImpl(std::make_unique<Impl>()) 
{} 
Widget::~Widget(){};

The reason we declare the destructor here is that we want to complete its definition in another file, which can reduce the workload of compiling with the Widget client. To put it bluntly, it is to isolate changes, which is like a design pattern.

And because we declared the destructor, the compiler will deprecate it by default, we need to customize:

#include<iostream>//*.h file
class Widget {
public:
	Widget();
	~Widget(); //declaration only
	Widget(Widget&& rhs) = default; 
	Widget& operator=(Widget&& rhs) = default; 
private:
	struct Impl;
	std::unique_ptr<Impl> pImpl;//use smart pointer
	//instead of raw pointer
};

But if we call the move construction, there will be the same problem, the compiler thinks that the class Impl we defined has only the declaration and no definition, so we need to write it below the class definition for the same problem.

#include<iostream>//*.h file
class Widget {
public:
	Widget();
	~Widget(); //declaration only
	Widget(Widget&& rhs)noexcept; 
	Widget& operator=(Widget&& rhs)noexcept;
private:
	struct Impl;
	std::unique_ptr<Impl> pImpl;
};
#include "test.h" // "*.cpp"
#include <string>
#include <vector>

struct Widget::Impl {
	std::string name; //as before
	std::vector<double> data;
};
Widget::Widget()
	:pImpl(std::make_unique<Impl>())
{} //std::unique_ptr
Widget::~Widget() {}; //~Widget definition
Widget::Widget(Widget&& rhs)noexcept =default;
Widget& Widget::operator=(Widget&& rhs)noexcept=default;

In actual use, this certainly supports copying, and it is not a simple shallow copy, but due to the existence of move construction and copying, the copy operation is discarded, so we need to write it ourselves:

#include<iostream>//*.h file
class Widget {
public:
	Widget();
	~Widget(); //declaration only
	Widget(Widget&& rhs)noexcept; 
	Widget& operator=(Widget&& rhs)noexcept;
	Widget(const Widget& w);
	Widget& operator=(const Widget& w);
private:
	struct Impl;
	std::unique_ptr<Impl> pImpl;//use smart pointer
	//instead of raw pointer
};
#include "test.h" // "*.cpp"
#include <string>
#include <vector>

struct Widget::Impl {
	std::string name; //as before
	std::vector<double> data;
};
Widget::Widget()
	:pImpl(std::make_unique<Impl>())
{} //std::unique_ptr
//via std::make_unique
Widget::~Widget() {}; //~Widget definition
Widget::Widget(Widget&& rhs)noexcept =default;
Widget& Widget::operator=(Widget&& rhs)noexcept=default;

Widget::Widget(const Widget& w) :pImpl(std::make_unique<Impl>(*w.pImpl))
{}

Widget& Widget::operator=(const Widget & w)
{
	*pImpl = *w.pImpl;
	return *this;
}

It is worth noting that if we use shared-ptr instead of unique-ptr, the default copy function will just meet our ideas, just use the default.

SFINAE mechanism

The SFINAE mechanism is a very important basis for the composition of the C++ template mechanism and type safety. The full name is Substitution failure is not an error. The general meaning is that as long as the available prototypes (such as function templates, class templates, etc.) are found, there will be no compilation errors. This property is used in generic programming.

explain

Explicitly specified template parameters are substituted before template parameter inference

Deduced arguments and arguments obtained from default values ​​will be replaced with deduced arguments for template arguments

Let's first look at a few simple examples:

template<typename T>
typename T::value_type sum(T a, T b, typename T::value_type c) 
{
	c = *a + *b;
	return c;
}
void main() 
{
	vector<int> v{ 1,2,3,4 };
	cout << sum(begin(v), end(v)-1, 0) << endl;
}

There is no doubt that the output is 5;

But if we want to call the following function, we will be warned by the compiler that no matching template parameter can be found.

void main() 
{
	std::vector<int> v{ 1,2,3,4 };
	cout << sum(begin(v), end(v)-1, 0) << endl;
	cout << sum(v.data(), v.data() + 3, 0) << endl;//error
}

To fix this, we had to write one more:

template<typename T>
T sum(T* a, T* b, T c) 
{
	c= *a + *b
	return c;
} 

This will compile and pass the test.

The problem here is that for the first function, the compiler tries to match the first template we wrote, but the matching fails. At this time, no error is reported, but it continues to search for template parameters that can be matched until it finds a template that can match and Until the best match, this is SFINAE, because the compiler often needs to try other possibilities when encountering Failure.

introducing

Next, let's talk about the complicated SFINAE situation. The purpose of SFINAE is to make the compiler reject code that cannot be compiled and choose the right code for dissimilar input types. For example, the following code is not asked, and there is no problem with static assertion.

template<typename T> struct add_ref { using type = T&; };
template<typename T>
using add_ref_t = typename add_ref<T>::type;

static_assert(std::is_same< add_ref_t< int >, int& >::value, "ops");
static_assert(std::is_same< add_ref_t< int&& >, int& >::value, "ops");
static_assert(std::is_same< add_ref_t< int&>, int& >::value, "ops");

We may take it for granted that this is true for all types, such as changing int to long, double, but except for one:

static_assert(!std::is_same< add_ref_t<void>, void >::value, "ops");

In fact, there is no reference to void, but the existence of void& is acquiesced here, and there will be problems in operation.

Then some people may say, well, let us set a partial specialization template for void, indeed, this can solve the problem:

template<> struct add_ref<void> { using type = void; };
template<> struct add_ref<const void> { using type = void; };
template<> struct add_ref<volatile void> { using type = void; };
template<> struct add_ref<const volatile void> { using type = void; };

But the problem is, in fact, the compiler can detect the problem, but we need such a partial specialization method for a better match, which is really thankless: today there is void, tomorrow there will be void2, void3, we can't always be biased Specialize. Is there a better solution?

In fact, we can ask the compiler whether our writing is reasonable through SFINAE:

template<class T> struct remove_ref { using type = T; };
template<class T> struct remove_ref<T&> { using type = T; };
template<class T> struct remove_ref<T&&> { using type = T; };
template<class T> using remove_ref_t = typename remove_ref<T>::type;


template<class T, class Enable> struct ALR { using type = T; }; //base template
template<class T> struct ALR<T,remove_ref_t<T&>> { using type = T&; }; //specialization
template<class T> struct add_l_ref:ALR<T, remove_ref_t<T>> {}; //point to use
template<class T> using add_l_ref_t = typename add_l_ref<T>::type;

It is easy to understand:

When we use the specialization, we are using the partial specialization part. If the second part in the <> is ill_formed, we can use the base template. In this case, no matter whether the T is well formed or not, we can handle that. then if we run the same code, it will be fine:

static_assert(std::is_same<add_l_ref_t<int>, int&>::value, "ops");//special
static_assert(std::is_same<add_l_ref_t<void>, void>::value, "ops");//base
static_assert(std::is_same<add_l_ref_t<int&&>, int&>::value, "ops");//special

viod_t (C++ 17)

is there any way to simplify this code,  which means a type expression always produces a simple well-known concrete type?

Yes, if we use void_t we can achieve that.

template<class...> using void_t = void;

If your compiler does not support c17, the code above is equal to the code following, which might be a little easier to understand.

template<class...T> struct temp {using type =void};
template<class...T> using void_t = typename temp<T...>::type;

with void_t, we can implement the above function with the following code:

template<class T, class Enable> struct ALR { using type = T; };// base
template<class T> struct ALR<T,void_t<T&>> { using type = T&; };//special
template<class T> struct add_l_ref:ALR<T, void> {};//point to use

This metafunction is used in template metaprogramming to detect ill-formed types in SFINAE context:

// primary template handles types that have no nested ::type member:
template< class, class = void >
struct has_type_member : std::false_type { };
 
// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };

Let us write some code to test them:

struct test 
{
	using type = int;
};

int main() 
{
	cout << has_type_member<test>() << endl;//sepcial
	cout<< has_type_member<int>() << endl;//base
}

the output is 1 and 0 separately;

 It can also be used to detect the validity of an expression:

// primary template handles types that do not support pre-increment:
template< class, class = void >
struct has_pre_increment_member : std::false_type { };
// specialization recognizes types that do support pre-increment:
template< class T >
struct has_pre_increment_member<T,
           std::void_t<decltype( ++std::declval<T&>() )>
       > : std::true_type { };

 let us test it:

struct test1 
{
	int i;
	test1& operator++() 
	{
		++this->i;
		return *this;
	}
};
struct test2
{
	int i;
};
int main() 
{
	cout << has_pre_increment_member<test2>() << endl;
	cout << has_pre_increment_member<test1>() << endl;
}

the output is 0 and 1 separately;

小结: 

第一次尝试用纯英文写,感觉挺痛苦的。

下一节写if constexpr(c++17)以及concept(c++20)。如果有其他篇幅加上CRTP以及memory order

  • 11
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无情の学习机器

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值