IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++ advanced(4)make function and SFINAE -> 正文阅读

[C++知识库]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

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-01-16 12:51:59  更:2022-01-16 12:52:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 10:54:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码