好用滴很!
前言
本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。
工具
需求分析
先放一段代码我们来看一下实际操作时可能会遇到的一些场景,并分析为什么我们会需要finally
语法。
上代码
#include <WinSock2.h>
#include <Windows.h>
#include <iostream>
std::string lastErrorMessage(int code = 0)
{
if (0 == code)
code = ::WSAGetLastError();
std::string result(1024, 0);
auto size = ::FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), result.data(), result.size(), nullptr);
result.resize(size);
return result;
};
void test01()
{
WSADATA wsaData{};
// initialization
auto errorCode = ::WSAStartup(MAKEWORD(2, 2), &wsaData);
if (errorCode)
throw std::runtime_error(lastErrorMessage(errorCode));
// create socket fd
auto fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == fd)
{
auto message = lastErrorMessage();
::WSACleanup();
throw std::runtime_error(std::move(message));
}
// configure server
SOCKADDR_IN serverConfig{};
serverConfig.sin_family = PF_INET;
serverConfig.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
serverConfig.sin_port = htons(11451);
// connecting to the server
if (connect(fd, reinterpret_cast<SOCKADDR *>(&serverConfig), sizeof(SOCKADDR)))
{
auto message = lastErrorMessage();
::closesocket(fd);
::WSACleanup();
throw std::runtime_error(std::move(message));
}
::closesocket(fd);
::WSACleanup();
}
int main()
{
std::cout << "[=] start test" << std::endl;
try
{
test01();
std::cout << "[+] test succeeded" << std::endl;
}
catch (const std::exception &e)
{
std::cout << "[-] " << e.what() << std::endl;
}
return 0;
}
代码中我们以常规socket
连接服务器的操作流程来演示。
可以看到在创建socket通信句柄与连接服务器失败时我们都需要对已初始化/创建
的资源进行释放,并且这一过程还伴随着诸多的冗余。例如WSACleanup
函数就需要多次书写,而这还只是一个小例子,在一个庞大的系统中可能会有非常多这样的冗余。
我们对三次调用WSACleanup
的地方进行分析,不难看出其都是在当前函数即将跳出作用域时才会执行。这就让我们联想到了RAII
对吧。
而基于RAII
实现的智能指针刚好就可以实现我们finally
的这个想法,我们可以利用std::unique_ptr
提供的自定义deleter
来实现finally
代码块。需要注意的是同为智能指针的std::shared_ptr
与std::weak_ptr
并不支持自定义的deleter
,所以没法用来实现finally
。
简易实现
接下来我们先来用std::unique_ptr
实现一个简单的finally
,上代码
void test01()
{
WSADATA wsaData{};
SOCKET fd{};
// construction the finally block
auto finallyBlock = [&](uint8_t *data)
{
if (nullptr != data)
delete data;
if (0 != fd && INVALID_SOCKET != fd)
::closesocket(fd);
::WSACleanup();
std::cout << "[=] finally block" << std::endl;
};
std::unique_ptr<uint8_t, decltype(finallyBlock)> defer(new uint8_t, finallyBlock);
// initialization
auto errorCode = ::WSAStartup(MAKEWORD(2, 2), &wsaData);
if (errorCode)
throw std::runtime_error(lastErrorMessage(errorCode));
// create socket fd
fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == fd)
throw std::runtime_error(lastErrorMessage(errorCode));
// configure server
SOCKADDR_IN serverConfig{};
serverConfig.sin_family = PF_INET;
serverConfig.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
serverConfig.sin_port = htons(11451);
// connecting to the server
if (connect(fd, reinterpret_cast<SOCKADDR *>(&serverConfig), sizeof(SOCKADDR)))
{
std::cout << "[-] connection failed" << std::endl;
throw std::runtime_error(lastErrorMessage());
}
}
我们对test01
函数进行一些修改,添加finally
实现与一些信息输出。
代码执行后效果如下
可以看到代码的执行顺序与我们预期的一样,顺序为
- 输出”connection failed”信息。
throw
连接服务器的异常信息。- 局部变量超出函数作用域
RAII
开始执行,智能指针defer
开始销毁所管理的裸指针uint8_t*
。 - 因为我们设置了自定义销毁函数(
deleter
),所以会进入finallyBlock
这个lambda
中。 - 执行释放资源的操作并输出信息”finally block”。
- 回到外部调用函数的地方(
main
)并处理异常,输出异常信息。
这样我们就实现了一个简易的finally
。
完整改进实现
上面说到的实现中虽然能用,但基本是不符合我们所设想的情况的。
首先他不是一个”新语法”,而是一种写法。
其次它的运行每次需要在堆中申请一个字节大小的内存并最终释放,如果编译器不能很好的优化的话这将造成很大的性能瓶颈。
new
与delete
的代价可不便宜啊,对于我们想单单执行一个lambda
表达式来说有点过于浪费了。
那么既然大家都是基于RAII
的话,何不自己造呢,老规矩先上代码后分析
#ifndef FINALLY_H //! FINALLY_H
#define FINALLY_H
#include <type_traits>
#include <utility>
template <typename callable_t>
class __FinallyCaller
{
public:
__FinallyCaller(callable_t callable)
: m_callable(std::move(callable))
{
}
~__FinallyCaller() noexcept
{
m_callable();
}
private:
callable_t m_callable;
};
struct __Helper
{
template <typename callable_t>
constexpr __FinallyCaller<callable_t> operator+(const callable_t &&callable)
{
static_assert(noexcept(callable), "The finally block cannot throw");
return {callable};
}
};
#define _MAKELAMBDA(x, y) x##y
#define MAKELAMBDA(x, y) _MAKELAMBDA(x, y)
#define finally auto &&MAKELAMBDA(lambda, __COUNTER__) = __Helper{} + [&]() noexcept
#endif //! FINALLY_H
首先我们来看__FinallyCaller
这个模板类,它的构造函数接收一个callable
的参数并存入成员变量中,并在析构函数中去调用这个callable
。
即当局部变量类型为__FinallyCaller
而又开始执行RAII
时,传进来的callable
会被调用,可以是函数也可以是lambda
。
那么我们来看一下__FinallyCaller
的使用方法
int main()
{
__FinallyCaller defer = [&]
{
std::cout << "[=] finally block" << std::endl;
};
std::cout << "[=] main end" << std::endl;
return 0;
}
/out:test_finally.exe
test_finally.obj
[=] main end
[=] finally block
可以看到流程与我们预想的一样。
但这种写法有一个问题,就是只支持c++17
及以上的版本,因为在c++17
才加入了类模板参数推导,而__Helper
就是解决这个问题的方法之一。
来看一下用法
int main()
{
auto defer = __Helper{} + [&]
{
std::cout << "[=] finally block" << std::endl;
};
std::cout << "[=] main end" << std::endl;
return 0;
}
/out:test_finally.exe
test_finally.obj
[=] main end
[=] finally block
没有问题,在c++11
c++14
中都能正常编译运行。
在这些用例中我们都还需要自己声明变量与构造,达不到我们finally
的需求,那么再来看看剩余的3个宏定义的工作。
首先是#define _MAKELAMBDA(x, y) x##y
,这个没啥好说的就是一个宏连接,将传入的x
和y
进行字面上的连接,比如1
和2
就会变成12
。
然后是#define MAKELAMBDA(x, y) _MAKELAMBDA(x, y)
,辅助展开,这个具体有啥用下面说明。
最后是#define finally auto &&MAKELAMBDA(lambda, __COUNTER__) = __Helper{} + [&]() noexcept
。
这条宏的本质是声明一个变量,来简化展开看一下就是auto lambda0 = __Helper{} + [&]
,其中这个变量名lambda0
后面跟的0
是由宏__COUNTER__
提供的一个全局计数器,使用MAKELAMBDA
进行连接而成。
这就要说到第二个宏辅助展开的作用,这里的__COUNTER__
如果不进行两层的MAKELAMBDA
包装的话是无法成功展开的,就会变成lambda__COUNTER__
这样的情况,这就与我们的预期相违,所以需要加一层。
那么有了这3条宏的加持我们基本上可以写出符合我们预期的代码了,我们直接对test01
函数进行修改来看一下最终效果
void test01()
{
WSADATA wsaData{};
SOCKET fd{};
// construction the finally block
finally
{
if (0 != fd && INVALID_SOCKET != fd)
::closesocket(fd);
::WSACleanup();
std::cout << "[=] finally block" << std::endl;
};
// initialization
auto errorCode = ::WSAStartup(MAKEWORD(2, 2), &wsaData);
if (errorCode)
throw std::runtime_error(lastErrorMessage(errorCode));
// create socket fd
fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == fd)
throw std::runtime_error(lastErrorMessage(errorCode));
// configure server
SOCKADDR_IN serverConfig{};
serverConfig.sin_family = PF_INET;
serverConfig.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
serverConfig.sin_port = htons(11451);
// connecting to the server
if (connect(fd, reinterpret_cast<SOCKADDR *>(&serverConfig), sizeof(SOCKADDR)))
{
std::cout << "[-] connection failed" << std::endl;
throw std::runtime_error(lastErrorMessage());
}
}
/out:test_finally.exe
test_finally.obj
[=] start test
[-] connection failed
[=] finally block
[-] 由于目标计算机积极拒绝,无法连接。
可以看到finally
的书写一下就变得简单了,没有那么多繁杂的东西,只有一个finally
关键字与{};
,大括号中即是我们需要最终执行的代码。
这个方法相比简易方法实现的最大的好处就是跟堆内存没有关系,这意味着在编译器的优化下可以被完全展开,这对于吃性能的场景是非常友好的。
唯一的缺点就是最后还要跟个;
,毕竟这是一条语句。如果能去掉这个;
的话就更好了。
结语
那么其实就这样了,也没有什么特别的难点,就是好用!
那就这样了,有缘再见~