赞
踩
本文我们还是来转述下cppcon的一篇演讲- “Back to Basics: C++ API Design - Jason Turner - CppCon 2022”,这篇演讲主要是讲述C++中设计API的一些细则,帮助我们写不容易出错的代码。Jason感觉很活跃,会场上到处跑,讲的东西也比较有用,请大家继续看下去。
Jason的演讲中反复强调的是想要设计的api很难被使用错误,这也是他所设计api的基本准则。
template<typename T>
class vector {
public:
bool empty() const;
};
首先抛出了这个例子,因为演讲中有大量的互动,这里大家也可以停下来想一想这个api的设计是不是很难使用错误?
有一些地方需要指出:
empty
里边是做了什么?(这里是说这个命名不太好)那么如何重写这个呢?
[[nodiscard]] bool is_empty() const;
那么错误处理呢,修改后的这个没有明确指出,还需要再次改动:
template<typename T>
class vector {
public:
[[nodiscard]] bool is_empty() const noexcept;
};
由上边的例子可以看出,设计一个好的api有哪些点注意。
计算机科学中最难的两件事是:
如果返回值被丢弃掉(不使用),编译器后产生一个警告,他可以应用函数声明和类型声明。
[[nodiscard]] int get_value();
int main() {
get_value(); // wanrning
}
也就是忽略掉get_value的返回值就会有一个警告,可以使用返回值接收或者传递参数等就不会报警告。
C++23
修复了一个小的漏洞,就是应用在lambda表达式上的[[nodiscard]
int main() {
auto l = [][[nodiscard]]() -> int {return 42;};
l(); // warning
}
struct [[nodiscard]] ErrorType{};
ErrorType get_value();
int main() {
get_value(); // warning
}
这里在结构体声明应用了[[nodiscard]]关键字,当ErrorType作为函数返回值时,对于该函数的调用,如果忽略了返回值则编译器会报一个警告。
[[nodiscard]]还可以应用在构造函数(C++20),来看一个例子:
struct FDHolder {
[[nodiscard]] FDHolder(int FD);
FDHolder();
};
int main() {
FDHolder{42}; // warning
FDHolder h{42}; // object no discard, no warning
FDHolder{}; // use default constructed, no warning
}
FDHolder有两个构造函数,一个是int参数,一个是默认构造函数。
当调用时,如果使用第一个构造函数构造对象,如果忽略构造的结构体对象,则会报警告,如果不忽略则不会报警告。同样使用没有被[[nodiscard]]
修饰的构造函数来构造对象也不会产生警告。
除此之外,使用[[nodiscard(“Lock object should never be discard”)]]用法来输出警告的内容。
noexcept会提示用户及编译器该函数不会抛出异常,但是如果这个函数运行过程中抛出异常了,那么std::terminate会被调用来结束进程。
void myfunc() noexcept {
throw 42;
}
int main() {
try {
myfunc();
} catch (...) {
// catch is irrelevant. 'terminate' is called
}
}
如果是这段代码在程序中,编译器可能会忽略掉try-catch
,或者留try-catch
在那里,但是都是会调用terminate来结束程序。
Widget* make_widget(int widget_type);
这个接口定义的怎么样,表意是否够明确,使用起来是否方便?
那么如何重写它呢?大家也可以考虑上边的总结和日常开发的经验做一下思考:
[[nodiscard]] std::unique_ptr<Widget> make_widget(int widget_type);
首先改写成以上,使用[[nodiscard]]修饰,返回值改成unique_ptr,这样可以传达的是,该函数创建的对象会将所有权进行转移到用户这里。
还可以做优化的;
enum class WidgetType {
Slider = 0,
Button = 1
};
[[nodiscard]] std::unique_ptr<Widget> make_widget(WidgetType type);
这里有话把输入改成了枚举,这样就限制了输入的范围。
哈,不过这里Jason又举了一个例子:
auto widget = make_widget(static_cast<WidgetType>(-42));
这真的是,猴子请来搞破坏的。然后Jason没有继续做优化,后边还会谈到。
截止到改写的目前为止,除了上边的问题,Jason抛出了错误处理:
错误处理机制分为抛出类的和非抛出类的,api设计的过程中要保持一致性。
std::expected
(C++23)或者其他更简单的FILE* fopen(const char *pathname, const char* mode);
最开始也还是抛出例子,大家先看这个原始api设计,再想想如何对其优化?
fopen("w", "/my/file/path")
如果这样调用会怎么样?(参数顺序调换位置)fopen("/my/file/path", 0)
如果这样调用呢?那么如何重写呢?
using FilePtr = std::unique_ptr<FILE,
decltype([](FILE* f){ fclose(f); })>;
[[nodiscard]] FilePtr fopen(const char* pathname, const char* mode);
这里首先对返回值进行优化,使用unique_ptr封装裸指针,保证其所有权转移,同时指定了该指针的析构函数。
可以再进一步优化:
using FilePtr = std::unique_ptr<FILE, decltype([](FILE* f){ fclose(f); })>;
[[nodiscard]] FilePtr fopen(const std::filesystem::path &path, std::string_view mode);
这样避免用户在调用的时候可以很明显的提示用户参数的类型不同,视觉上对参数的具体位置有一些提示。但是这样还是会有问题,因为无论是std::filesystem::path
还是std::string_view
都可以传递字符串隐式转化。
auto file = fopen("w", "/my/file/path")
类似这样还是可以调用成功的。那么假设std::filesystem::path
还是std::string_view
是最正确的类型了,那么我们该如何优化呢?
void fopen(const auto&, const auto&) = delete;
这个是c++20引入的隐式模板,也就是说不需要写template等关键字,其实他这里表述的意思是,这个隐式模板会捕捉到所有的类型,除了我们前边声明的fopen函数,进一步说就是如果不是明确的传递参数类型是std::filesystem::path
和std::string_view
那么就会被这个隐式模板捕捉到,但是它是个delete
的函数,那么就会编译不过。
那么这样就能很明显的避免掉类型传递容易被交换位置的问题了。
std::filesystem::path
及std::string_view
不是强类型,即可以发生隐式转换const char*
,string
,string_view
及path
之间的隐式转化会打破类型的安全性。=delete
删除有问题的重载=delete
=delete
掉一个模板,他就会匹配所有不明确的参数,阻止隐式转化这里Jason又回到了工厂函数那里,看是否还可以优化下,因为遗留了一个问题:
auto widget = make_widget(static_cast<WidgetType>(-42));
这里能够以强类型的方法再一次对其优化:
template<typename WidgetType>
[[nodiscard]] WidgetType make_widget() requires (std::is_base_of_v<Widget, WidgetType>);
使用requires限定了WidgetType必须是Widget的子类,这样就强类型限制了创建的对象,用户只能传递Widget的子类来构造。
然后再回来看fopen的最开始的例子?
FILE* fopen(const char *pathname, const char* mode);
void use_string(std::string * const * str) {
if (str) { // str is optional
// do something
} else {
// do other things
}
}
其实这里想要表达的是使用裸指针错误函数参数,你的API内部就需要做判断,不做判断直接使用就会有不安全的问题。同时如果用户使用该api传递了错误的指针,这里就会有未定义的错误。
所以好的方法是传递引用:
// no-trivial. pass by (const) reference
void use_string(const std::string& str) {
puts(str.c_str());
}
// trivial and small, copy it
void use_int(const int i) {
// use i
}
最后Jason又回到了优化后的fopen
这里:
using FilePtr = std::unique_ptr<FILE, decltype([](FILE* f){ fclose(f); })>;
[[nodiscard]] FilePtr fopen(const std::filesystem::path &path, std::string_view mode);
这里讨论到针对mode的可能的输入集,我们如何对这个api进行优化呢,这里是一个开放的结局,他提了几个方案参考:
最后我把Jason总结原则罗列在这里:
[[nodiscard]]
(可以加上原因)noexcept
来表示函数没有异常抛出=delete
来阻止转换到这里结束了,感谢阅读,搬运不易,点个赞吧
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。