在编程时,我们经常会遇到可能会返回/传递/使用一个确定类型对象的场景。也就是说,这个对象可能有一个确定类型的值也可能没有任何值。因此,我们需要一种方法来模拟类似指针的语义:通过nullptr
表示指针为空。解决方法就是定义该对象的同时再定义一个附加的bool
类型来标志该对象有没有值。std::optional
就是提供了一种类型安全的方式来实现。
#include
optional<int> aslnt(const string& s)
{
try
{
return stoi(s);
}
catch(...)
{
return nullopt;
}
}
int main()
{
for (auto s : { "42","077","hello","0x33" })
{
optional<int> oi = aslnt(s);
if (oi)
cout << "convert " << s << " to int: " << *oi << endl;
else
cout << "can't convert " << s << endl;
}
}
我们实现一个函数,用来对字符串转换为整形。这个操作可能会失败,因此,在失败的时候,我们返回一个nullopt
表示没有int值。并且,通过解引用*
来获取optional
对象的值。除了这种方式,还有has_value()
和value()
分别是判断是否有值和值是多少,更为安全。
int main()
{
for (auto s : { "42","077","hello","0x33" })
{
optional<int> oi = aslnt(s);
if (oi.has_value())
cout << "convert " << s << " to int: " << oi.value() << endl;
else
cout << "can't convert " << s << endl;
}
}
另一个例子是传递可选的参数或者设置可选的数据成员:
class Name
{
private:
string first;
optional<string> middle;
string last;
public:
Name(string f,optional<string> m,string l)
: first{f},middle{m},last{l}
{}
friend ostream& operator<<(ostream& strm, const Name& n)
{
strm << n.first << ' ';
if (n.middle)
{
strm << *n.middle << ' ';
}
return strm << n.last;
}
};
还有一个访问值的方法,value_or()
,当没有值的时候可以制定一个备选值。比如:
cout << middle.value_or("");
optional类
template <class T>
class optional
{};
还定义了这些类型和对象:
nullopt_t
类型的nullopt
,表示可选对象无值。bad_optional_access
异常类,当无值的时候访问会抛出该异常。操作符 | 效果 |
---|---|
make_optional<>() | 创建一个用参数初始化的可选对象。 |
emplace() | 给内含类型赋予一个新值。 |
reset() | 销毁值,使其变成无值状态。 |
has_value() | 是否有值。 |
value() | 访问内部值(如果无值会抛出异常) |
value_or() | 访问内部值(如果无值,将返回参数的值) |
swap() | 交换两个对象的值。 |
in_place | 构造函数的第一个参数,如果要用多个参数初始化可选对象,必须将这个当做第一个参数传递。 |
案例:
int main()
{
optional<int> o1;
optional<int> o2(nullopt);
optional o3{ 42 };
optional o4{ complex{3.0, 4.0} };
optional < complex<double>> o5{ in_place, 3.0, 4.0 };
auto o5 = make_optional(3.0);
if (o5) // true
if (!o5) // false
if (o5.has_value()) // true
cout << o5; // ERROR
cout << *o5 << endl; // 3.0
cout << o5.value() << endl; // 3.0
cout << o1.value_or(1); // 1
o1 = 3;
o1.emplace(3); // 等价于上面
o5.reset(); // o5==nullopt
optional<int> o6 = move(o5); // o5==nullopt
}
一些特定的可选类型可能会导致特殊或意料之外的行为。
optional<bool> ob{false}; // false
if(!ob) // false
if(ob == false) // true
optional<int*> op{nullptr};
if(!op) // false
if(op == nullptr) // true
理论上,你可以定义可选对象的可选对象:
int main()
{
optional<optional<string>> oos1;
optional<optional<string>> oos2 = "hello";
optional<optional<string>> oos3{in_place,in_place,"hello"};
oos1 = "hello";
cout << (*oos1 == nullopt) << endl; // 内层可选对象无值
cout << (oos1 == nullopt) << endl; // 外层可选对象无值
}
variant
是C++标准库提供的一个新的联合类型,它最大的优势是提供了一种新的具有多态性的处理异质集合的方法。也就是说,它可以帮助我们处理不同类型的数据,并且不需要公共基类和指针。
起源于C,C++也提供了union
的支持,它的作用是持有一个值,这个值的类型可能是指定的若干类型中的任意一个。然而,这项语言特性有一些缺陷:
string
。union
派生。通过variant
,C++标准库提供了一种可辨识的联合。
事实上,variant
持有的值有若干候选项
,这些选项通常有不同的类型。然而,两个不同选项的类型也有可能相同,这在多个类型相同的选项分别代表不同的含义是很有用。
variant
的内存大小等于所有可能的底层类型中最大的再加上一个记录当前选项的固定内存开销。
int main()
{
variant<int, string> var{ "hi" };
cout << var.index() << endl; // 1
var = 42;
cout << var.index() << endl; // 0
try
{
int i = get<0>(var); // 通过索引访问
string s = get<string>(var); // 通过类型访问
}
catch (const bad_variant_access& e)
{
cerr << "EXCEPTION: " << e.what() << endl;
}
}
index()
成员函数可以指出当前选项的索引。
初始化和赋值操作都会查找最匹配的选项。如果类型不能精确匹配,可能会发生奇怪的事情。
注意variant
不存在空、有引用成员、有C风格数组成员、有不完全类型(void)。
如果在初始化时,并没有赋初值,那么会将第一个参数作为选项,并调用这个类型对应的默认构造函数,如果没有默认构造,那么会导致编译期错误。
struct A
{
A(int i)
{
cout << "A(int i)" << endl;
}
};
variant<A,int> v1; // ERROR
辅助类型std::monostate
提供了处理这种情况的能力,还可以用来模拟空值的状态。
它的作用是可以作为variant
的一个选项,表示variant
没有其他任何类型的值。所以,可以保证variant
能够默认构造。
variant<monostate,A,int> v2; // OK
cout << v2.index() << endl; // 0
你可以从variant
派生:
class Derived : public variant<int,string>
{};
Derived d = {{"hello"}};
cout << d.index() << endl; //1
cout << get<1>(d) << endl; // hello
d.emplace<0>(77); // 初始化int,销毁string
cout << get<0>(d) << endl;
template<typename... Types>
class variant;
此外,还定义了下面的类型和对象:
variant_size
。variant_alternative
。variant_npos
。monostate
bad_variant_access
。操作符 | 效果 |
---|---|
emplace() | 销毁旧值并赋一个T类型选项的新值。 |
emplace() | 销毁旧值并赋一个索引为Idx 的选项的新值。 |
valueless_by_exception() | 返回变量是否因为异常而没有值。 |
index() | 返回当前选项的索引。 |
swap() | 交换两个对象的值。 |
holds_alternative() | 返回是否持有类型T的值。 |
get() | 返回类型为T的选项的值。 |
get() | 返回索引为Idx的选项的值。 |
get_if() | 返回指向类型为T的选项的指针或nullptr 。 |
get_if() | 返回指向索引为Idx的选项的指针或nullptr 。 |
visit() | 对当前选项进行操作。 |
案例:
int main()
{
variant<int, int, string> v1; // 默认构造
variant<long, int> v2{ 42 };
cout << v1.index() << endl; // 1
// 如果有两个类型同等匹配会导致歧义
// variant v3{ 42 }; // ERROR
// 为了传递多个值调用构造初始化
variant<complex<double>> v4{ in_place_type<complex<double>>,3.0,4.0 };
variant<complex<double>> v5{ in_place_index<0>,3.0,4.0 };
// 访问值
variant<int, int, string> var;
auto a = get<double>(var); // ERROR
// 如果访问失败,返回nullptr,如果访问成功,返回当前选项的指针
if (auto ip = get_if<1>(&var); ip != nullptr)
cout << *ip << endl;
// 修改值
var = "hello";
var.emplace<1>(42);
}
另一个处理variant
对象的值的方法就是使用访问器
。访问器为每一个可能的类型提供一个函数调用运算符的对象。当这些对象访问一个variant
时,就会调用和当前选项最匹配的函数。
struct MyVisitor
{
void operator()(int i) const
{
cout << "int: " << i << endl;
}
void operator()(string s) const
{
cout << "string: " << s << endl;
}
void operator()(long double d) const
{
cout << "long double: " << d << endl;
}
};
int main()
{
variant<int, string, long double> var{ 42 };
visit(MyVisitor(), var); // 调用operator()(int)
var = "hello";
visit(MyVisitor(), var); // 调用operator()(string)
var = 42.7;
visit(MyVisitor(), var); // 调用operator()(long double)
}
如果访问器没有某一个可能的类型的operator()
重载,那么visit()
会导致编译期错误。如果有歧义也会导致编译器错误。
最简单的方式可以使用泛型lambda来作为访问器。
visit([](auto& val)
{
cout << val << endl;
},var);
访问器的函数也可以返回值,但所有返回值类型必须相同。例如:
int main()
{
using IntOrDouble = variant<int, double>;
vector<IntOrDouble> coll{ 42,7.7,0,-0.7 };
double sum{ 0 };
for (const auto& elem : coll)
{
sum += visit([](const auto& val) -> double
{
return val;
}, elem);
}
cout << sum << endl;
}
通过使用函数对象和lambda
的重载器,可以定义一系列的lambda,其中最匹配的会被用作访问器。
假设有一个如下定义的重载器:
template<typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
template<typename... Ts>
overload(Ts...)->overload<Ts...>;
int main()
{
variant<int, string> var(42);
visit(overload{
[](int i) { cout << i << endl; },
[](const string& s) { cout << s << endl; },
}, var);
}
如果你赋给一个variant
新值时发送了异常,那么这个variant
可能会进入一个非常特殊的状态,失去了旧值并且没有获取新的值。
如果遇到这种情况,那么:
var.valueless_by_exception()
会返回true
。var.index()
会返回variant_npos
。class Circle
{
private:
Coord center;
int rad;
public:
Circle(Coord c, int r) : center{ c }, rad{ r } {}
void move(const Coord& c)
{
center += c;
}
void draw() const
{
cout << "circle at " << center << " with radius " << rad << endl;
}
};
class Rectangle {
// ...
};
class Line {
// ...
};
class Coord {
// ...
};
using GeoObj = variant<Line, Circle, Rectangle>;
// 创建并初始化一个几何体对象的集合
vector<GeoObj> createFigure()
{
vector<GeoObj> f;
f.push_back(Line{ Coord{1,2},Coord{3,4} });
f.push_back(Circle{ Coord{5,5},2 });
f.push_back(Rectangle{ Coord{3,3},Coord{6,4} });
return f;
}
int main()
{
vector<GeoObj> figure = createFigure();
for (const GeoObj& geoobj : figure)
{
visit([](const auto& obj)
{
obj.draw(); // 多态性调用draw
}, geoobj);
}
}
如果这些实例有一个不能编译,那么对visit
的调用也不能编译。从效率上来讲,这种行为和虚函数表的行为相同。但是draw
不是虚函数。
int main()
{
using Var = variant<int, double, string>;
vector<Var> values{ 42,0.19,"hello world",0.815 };
for (const Var& val : values)
{
visit([](const auto& v)
{
if constexpr (is_same_v<decltype(v), const string&>)
cout << "\"" << v << '\" ';
else
cout << v << ' ';
}, val);
}
}
总结一下使用variant
实现的异构集合的优点和缺点:
优点:
virtual
成员函数。vector
是连续存放的。缺点:
如果一个variant
有bool
和string
选项,赋予一个字符串字面量可能会导致令人惊奇的事,因为字符串字面量会优先转换为bool
,而不是string
。例如:
variant<bool,string> v;
v = "hi";
cout << "index: " << v.index() << endl; // 0
可以使用以下的方式解决:
v.emplace<1>("hello");
v.emplace<string>("hello");
一般来说,C++是一门类型绑定和类型安全的语言。值对象被声明为确定的类型,这个类型定义了所有可能的操作、也定义了对象的行为。而且,对象不能改变自身的类型。
std::any
是一种在保证类型安全的基础上还能改变自身类型的值类型。也就是说,它可以持有任意类型的值,并且知道自己当前的值是什么类型的。
实现的关键在于std::any
对象内包含了值和值的类型。
对于any
对象,如果你赋值一个字符串,它将会分配内存并拷贝字符串,并且存储记录当前的值为一个字符串。之后,可以使用运行时检查来判断当前的值类型。为了将当前的值转换为真实的类型,必须使用any_cast<>
。
int main()
{
any a;
any b = 4.3;
a = 42;
b = string{ "hi" };
if (a.type() == typeid(string))
{
string s = any_cast<string>(a);
useString(s);
}
else if (a.type() == typeid(int))
{
useInt(any_cast<int>(a));
}
}
通过使用成员type()
,可以检查内含值的类型与某一个类型的ID是否相同。如果对象是空的,那么type() == typeid(void)
,为了访问内部的值,必须使用any_cast
转换为真正的类型,如果转换失败,会抛出bad_any_cast
异常。
除此之外,还有reset()
清空对象,has_value()
判断是否有值。
C++17,引入了一个类型来代表内存的最小单位:字节。byte
本质上代表一个字节的值,不能进行数字或字符的操作,这样会更加类型安全。
#include
int main()
{
byte b1{ 0x3F };
byte b2{ 0b11110000 };
byte b3[4]{ b1,b2,byte{1} };
if (b1 == b3[0])
b1 <<= 1;
cout << to_integer<int>(b1) << endl; // 126;
}
enum class byte : unsigned char {};
// 支持位运算
template <class IntType, enable_if_t<is_integral_v<IntType>, int> = 0>
[[nodiscard]] constexpr byte operator<<(const byte b, const IntType shift) noexcept {
// every static_cast is intentional
return static_cast<byte>(static_cast<unsigned char>(static_cast<unsigned int>(b) << shift));
}
template <class IntType, enable_if_t<is_integral_v<IntType>, int> = 0>
[[nodiscard]] constexpr byte operator>>(const byte b, const IntType shift) noexcept {
// every static_cast is intentional
return static_cast<byte>(static_cast<unsigned char>(static_cast<unsigned int>(b) >> shift));
}
[[nodiscard]] constexpr byte operator|(const byte _Left, const byte _Right) noexcept {
// every static_cast is intentional
return static_cast<byte>(
static_cast<unsigned char>(static_cast<unsigned int>(_Left) | static_cast<unsigned int>(_Right)));
}
[[nodiscard]] constexpr byte operator&(const byte _Left, const byte _Right) noexcept {
// every static_cast is intentional
return static_cast<byte>(
static_cast<unsigned char>(static_cast<unsigned int>(_Left) & static_cast<unsigned int>(_Right)));
}
[[nodiscard]] constexpr byte operator^(const byte _Left, const byte _Right) noexcept {
// every static_cast is intentional
return static_cast<byte>(
static_cast<unsigned char>(static_cast<unsigned int>(_Left) ^ static_cast<unsigned int>(_Right)));
}
[[nodiscard]] constexpr byte operator~(const byte b) noexcept {
// every static_cast is intentional
return static_cast<byte>(static_cast<unsigned char>(~static_cast<unsigned int>(b)));
}
template <class IntType, enable_if_t<is_integral_v<IntType>, int> = 0>
constexpr byte& operator<<=(byte& b, const IntType shift) noexcept {
return b = b << shift;
}
template <class IntType, enable_if_t<is_integral_v<IntType>, int> = 0>
constexpr byte& operator>>=(byte& b, const IntType shift) noexcept {
return b = b >> shift;
}
constexpr byte& operator|=(byte& _Left, const byte _Right) noexcept {
return _Left = _Left | _Right;
}
constexpr byte& operator&=(byte& _Left, const byte _Right) noexcept {
return _Left = _Left & _Right;
}
constexpr byte& operator^=(byte& _Left, const byte _Right) noexcept {
return _Left = _Left ^ _Right;
}
template <class IntType, enable_if_t<is_integral_v<IntType>, int> = 0>
[[nodiscard]] constexpr IntType to_integer(const byte b) noexcept {
return static_cast<IntType>(b);
}
操作 | 效果 |
---|---|
位运算(<<,>>,|,&,^) | 字节的位运算 |
比较操作 | 字节的比较 |
to_integer() | 可以把字节转换为基本类型 |
sizeof() | 1 |
C++17中,C++标准库引入了一个特殊的字符串类string_view
,它能够让我们像处理字符串一样处理字符串序列,不需要为它们分配空间。也就是说string_view
类型的对象只是引用一个外部的字符串序列,不需要持有它们。
和string
相比,string_view
对象有如下特点:
data()
返回的可能是nullptr
。字符串视图有两个主要的应用:
#include
template<typename T>
void printElems(const T& coll, string_view prefix = {})
{
for (const auto& elem : coll)
{
if (prefix.data())
cout << prefix << ' ';
cout << elem << endl;
}
}
string_view
和string
比起来,可能会减少一次分配堆内存的调用。
通常智能指针
会比相应的语言特性更为安全。因此,你可能会认为字符串视图比字符串引用更为安全,然而,事实上,字符串视图和原生字符指针一样危险。
不要把临时字符串赋值给字符串视图。
string_view retString();
string_view sv = retString(); // 不延长返回值的生命周期
返回值类型是字符串视图时不要返回字符串。
class Person
{
string name;
public:
string_view getName() const
{
return name;
}
};
函数模板应该使用auto作为返回值类型。
// 为字符串视图定义+,返回string
string operator+(string_view sv1,string_view sv2)
{
return string(sv1) + string(sv2);
}
// 泛型连接函数
template<typename T>
auto operator+(const T& sv1,const T& sv2)
{
return x + y;
}
string_view hi = "hi";
auto xy = concat(hi,hi);
不要在调用链中使用字符串视图来初始化字符串。
class Person
{
string name;
public:
Person(string_view n) // 不要这样做
:name{n}
{}
};
string s = "Joe";
Person p {move(s)}; // 性能开销
总结:
string
的API使用string_view
。string_view
形参来初始化string成员。在头文件
中,C++为basic_string_view<>
提供了很多特化版本:
using string_view = basic_string_view<char>;
using u16string_view = basic_string_view<char16_t>;
using u32string_view = basic_string_view<char32_t>;
using wstring_view = basic_string_view<wchar_t>;
操作 | 效果 |
---|---|
swap() | 交换两个字符串视图的值 |
empty() | 判断字符串视图是否为空 |
size()/length() | 返回字符的数量 |
max_size() | 返回可能最大字符数 |
front()/back() | 返回第一个字符或者最后一个字符 |
copy() | 把内容拷贝或写入到字符数组 |
data() | 返回nullptr或常量字符数组(没有空字符结尾) |
begin()/end() | 返回起始位置和末尾位置的下一位的迭代器 |
substr() | 返回子字符串 |
remove_prefix() | 移除开头的若干字符 |
remove_suffix() | 移除末尾的若干字符 |
案例:
int main()
{
string_view sv;
auto p = sv.data(); // nullptr
sv = "hello";
cout << sv << endl;
cout << sv.size() << endl; // 5
using namespace literals;
auto s = "hello"sv; // string_view
s.swap(sv);
string_view sv2 = "I like my kindergarten";
sv2.remove_prefix(2);
sv2.remove_suffix(8);
cout << sv2 << endl; // like my kind
}
字符串视图开销很小并且每一个string
都可以用作字符串视图。但是,只有当函数按照如下约束使用参数时,字符串视图才有意义:
const char*
为参数而没有长度参数的C函数就不属于这种情况。nullptr
的情况。// 带前缀输出时间点
void print(const string& prefix, const chrono::system_clock::time_point& tp)
{
// 转换为日历时间
auto rawtime{ chrono::system_clock::to_time_t(tp) };
string ts{ std::ctime(&rawtime) };
ts.resize(ts.size() - 1); // 跳过末尾的换行符
cout << prefix << ts;
}
// 替换成下面代码
void print(string_view prefix, const chrono::system_clock::time_point& tp)
{
// 转换为日历时间
auto rawtime{ chrono::system_clock::to_time_t(tp) };
string_view ts{ std::ctime(&rawtime) };
ts.remove_suffix(1); // 跳过末尾的换行符
cout << prefix << ts;
}
最先想到的就是吧只读字符串引用换成字符串视图,只要我们不使用会因为没有值或者没有空字符终止而失败的操作就可以了。
同时,我们也对内部ctime()
的返回值使用了字符串视图。这个值只有在下一次ctime()
或者asctime()
调用之前有效。多线程环境下,这个函数将导致问题。
如果要将结果返回:
string print(string_view prefix, const chrono::system_clock::time_point& tp)
{
// 转换为日历时间
auto rawtime{ chrono::system_clock::to_time_t(tp) };
string_view ts{ std::ctime(&rawtime) };
ts.remove_suffix(1); // 跳过末尾的换行符
return string{ prefix } + string{ ts }; // 字符串视图没有重载+
}
C++17,Boost.Filesytem
终于被C++标准所采纳,还进行了很多调整和改进。
#include
#include
#include
using namespace std;
int main(int argc,char *argv[])
{
if (argc < 2)
{
cout << "Usage: " << argv[0] << " \n" ;
return EXIT_FAILURE;
}
filesystem::path p{ argv[1] }; // p代表的是一个文件系统路径
if (filesystem::is_regular_file(p)) // 判断p是否是一个普通路径
{
cout << p << " exists with " << file_size(p) << " bytes" << endl;
}
else if (filesystem::is_directory(p)) // p是一个目录吗
{
cout << p << " is a directory containing:\n";
for (const auto& e : filesystem::directory_iterator{ p }) // 遍历该目录下的所有文件
cout << " " << e.path() << endl;
}
else if (filesystem::exists(p)) // 路径p是否存在
{
cout << p << " is a special file\n";
}
else
{
cout << "path: " << p << " does not exist\n";
}
}
在windows下处理路径:
默认情况下,输出路径时用双引号括起来并用反斜杠转义,反斜杠会导致一个问题。
# 输入
checkpath C:\
# 输出
"C:\\" is a directory containing:
...
"C:\\Users"
因此,可以使用成员函数string()
。
int main(int argc, char* argv[])
{
if (argc < 2)
{
cout << "Usage: " << argv[0] << " \n" ;
return EXIT_FAILURE;
}
filesystem::path p{ argv[1] }; // p代表的是一个文件系统路径
if (is_regular_file(p)) // 判断p是否是一个普通路径
{
cout << p.string() << " exists with " << file_size(p) << " bytes" << endl;
}
else if (is_directory(p)) // p是一个目录吗
{
cout << p.string() << " is a directory containing:\n";
for (const auto& e : filesystem::directory_iterator{ p }) // 遍历该目录下的所有文件
cout << " " << e.path().string() << endl;
}
else if (exists(p)) // 路径p是否存在
{
cout << p.string() << " is a special file\n";
}
else
{
cout << "path: " << p.string() << " does not exist\n";
}
}
int main(int argc, char* argv[])
{
if (argc < 2)
{
cout << "Usage: " << argv[0] << " \n" ;
return EXIT_FAILURE;
}
namespace fs = filesystem;
switch (fs::path p{ argv[1] };status(p).type())
{
case fs::file_type::not_found:
cout << "path \"" << p.string() << "\" does not exist\n";
break;
case fs::file_type::regular:
cout << "path \"" << p.string() << "\" exists with " << file_size(p) << "\n";
break;
case fs::file_type::directory:
cout << "\"" << p.string() << "\" is a directory containing:\n";
for (const auto& e : filesystem::directory_iterator{ p }) // 遍历该目录下的所有文件
cout << " " << e.path().string() << endl;
break;
default:
cout << "\"" << p.string() << "\" is a special file\n";
break;
}
}
status().type()
返回一个file_type
,是一个枚举类,包含以下枚举值:
enum class file_type
{
none,
not_found,
regular,
directory,
symlink,
block, // not used on Windows
character, // not used in this implementation; theoretically some special files like CON
// might qualify, but querying for this is extremely expensive and unlikely
// to be useful in practice
fifo, // not used on Windows (\\.\pipe named pipes don't behave exactly like POSIX fifos)
socket, // not used on Windows
unknown,
junction // implementation-defined value indicating an NT junction
};
#include
int main()
{
namespace fs = filesystem;
try
{
// 创建目录tmp/test
fs::path testDir{ "tmp/test" };
fs::create_directories(testDir); // 可以递归创建整个目录下缺少的文件或者目录 如果已经存在会抛出异常
// 创建数据文件/tmp/test/data.txt
auto testFile = testDir / "data.txt"; // 重载了'/'
ofstream dataFile{ testFile };
if (!dataFile)
{
cerr << "OOPS,can't open \"" << testFile.string() << "\"\n";
exit(EXIT_FAILURE);
}
dataFile << "The answer is 42\n";
// 创建符号连接tmp/slink/,指向tmp/test/:
// 第一个参数是将创建的符号连接所在的目录为起点的相对路径
// 第二个参数是指向的符号链接的路径
fs::create_directory_symlink("test", testDir.parent_path() / "slink");
}
catch (fs::filesystem_error& e)
{
cerr << "EXCEPTION: " << e.what() << endl;
cerr << " path1:\"" << e.path1().string() << endl;
}
// 递归列出所有文件
cout << fs::current_path().string() << ":\n";
auto iterOpts{ fs::directory_options::follow_directory_symlink }; // 遍历符号链接的选项
for (const auto& e : fs::recursive_directory_iterator(".", iterOpts))
{
cout << " " << e.path().lexically_normal().string() << endl; // lexically_normal()可以去掉绝对路径的"./"
}
}
C++标准库不仅标准化了所有的操作系统的文件系统中的公共部分,在很多情况下,C++标准还尽可能的遵循POSIX的标准的要求来实现。比如:
不同的文件系统的差异也有个纳入考虑:
通常文件系统是标准命名空间下的filesystem
子命名空间。可以使用namespace fs = std::filesystem
作为缩写。
文件系统库的一个关键元素是path
。它代表文件系统中某一个文件的位置。它由可选的根名称、可选的根目录和一些以目录分隔符分隔的文件名组成。路径可以是相对的也可以是绝对的。
路径可能有不同的格式:
一些特殊的文件名:
.
代表当前路径。..
代表父目录。路径可以正规化,在正规化的路径中:
.
,否则路径不会使用.
。..
。.
或者..
,否则路径结尾的文件名是目录时要加上目录分隔符。文件系统提供了一些函数,有些是成员函数有些是独立函数,这么做的目的是:
成员函数开销较小。不需要进行系统调用,例如:
mypath.is_absolute() ; // 检查路径是否是绝对的
独立函数开销较大。因为会访问实际的文件系统,意味着要进行系统调用。
equivalent(path1,path2); // 如果两个路径指向同一个文件返回true
文件系统是错误的根源。你必须考虑相应的文件是否存在、文件操作是否被允许、该操作是否会违背资源限制。另外,当程序运行时其他进程可能创建、修改、或者移除了某些文件,意味着事先检查并不能保证没有错误。
文件系统使用了混合的异常处理方式:
filesystem_error异常
:
try
{
if(!create_directory(p))
cout << p << " already exists\n";
}
catch(fs:filesystem_error& e)
{
cerr << "EXCEPTION: " << e.what() << endl;
cerr << " path1:\"" << e.path1().string() << endl; // 获取错误相关的第一个路径
}
error_code
参数:
error_code ec; // C++11引入
create_directory(p,ec); // 发送错误设置错误码
if(ec)
{
cout << ec.message() << endl;
}
// 检查特定的错误码
if(ec == errc::read_only_file_system) // 文件只能只读
{
// ...
}
不同的操作系统支持不同的文件类型。它定义了一个枚举类型file_type
,标准定义了如下的值。
值 | 含义 |
---|---|
regular | 普通文件 |
directory | 目录文件 |
symlink | 符号链接文件 |
character | 字符特殊文件 |
block | 块特殊文件 |
fifo | FIFO或管道文件 |
socket | 套接字文件 |
none | 文件类型未知 |
unknown | 文件存在但推断不出类型 |
not_found | 文件不存在 |
有很多处理文件系统的操作。这些操作涉及到的关键类型都是std::filesystem::path
,它表示一个可能存在也可能不存在的文件的绝对或相对路径。
调用 | 效果 |
---|---|
path{charseq} | 用一个字符序列初始化路径 |
path{beg,end} | 用一个范围初始化路径 |
u8path(u8string) | 用一个UTF-8字符串初始化路径 |
current_path() | 返回当前工作目录的路径 |
temp_directory_path() | 返回临时文件的路径 |
路径p
可以调用的函数。这些操作不会访问底层的系统调用。
调用 | 效果 |
---|---|
empty() | 返回路径是否为空 |
is_absolute() | 返回路径是否是绝对的 |
is_relative() | 返回路径是否是相对的 |
has_filename()/has_stem() | 返回路径是否既不是目录也不是根名称 |
has_extension() | 返回路径是否有扩展名 |
has_root_name() | 返回路径是否包含根名称 |
has_root_directory() | 返回路径是否包含根目录 |
has_root_path() | 返回路径是否包含根名称或者根目录 |
has_parent_path() | 返回路径是否包含父路径 |
has_relative_path() | 返回路径是否不止包含根元素 |
filename() | 返回文件名(或者空路径) |
stem() | 返回没有扩展名的文件名(或者空路径) |
extension() | 返回拓展名(或者空路径) |
root_name() | 返回根名称(或者空路径) |
root_directory() | 返回根目录(或者空路径) |
root_path() | 返回根元素(或者空路径) |
parent_path() | 返回父路径(或者空路径) |
relative_path() | 返回不带根元素的路径(或者空路径) |
begin() | 返回路径元素的起点 |
end() | 返回路径元素的终点 |
你可以遍历一个路径,这将会返回路径的所有元素:根名称、根目录、所有的文件名。
路径迭代器是双向迭代器。迭代器的值的类型是path
。
打印路径:
void printPath(const filesystem::path& p)
{
cout << "path elements of\"" << p.string() << "\":\n";
for (filesystem::path elem : p)
{
cout << " \"" << elem.string() << "\"";
}
cout << endl;
}
调用 | 效果 |
---|---|
strm << p | 用双引号括起来输出路径 |
strm >> p | 读取用双引号括起来的路径 |
string() | 以字符串返回路径 |
wstring() | 以宽字符串返回路径 |
u8string() | 以UTF-8字符串返回路径 |
u16string() | 以UTF-16字符串返回路径 |
u32string() | 以UTF-32字符串返回路径 |
lexically_normal() | 返回正规化的路径 |
lexically_relative(p2) | 返回从p2到p的相对路径(如果没有则返回空路径) |
lexically_proximate(p2) | 返回从p2到p的路径(如果没有返回p) |
代码案例:
int main()
{
namespace fs = std::filesystem;
fs::path p{ "/dir/./sub//sub1/../sub2" };
cout << "path: " << p << endl; // "/dir/./sub//sub1/../sub2"
cout << "string(): " << p.string() << endl; // /dir/./sub//sub1/../sub2
cout << "lexically_normal(): " << p.lexically_normal() << endl; // "\\dir\\sub\\sub2" 根据系统决定
// 计算相对路径
fs::path{ "/a/d" }.lexically_relative("/a/b/c"); // "../../d"
fs::path{ "/a/b/c" }.lexically_relative("/a/d"); // "../b/c"
// windows
fs::path{ "C:/a/b" }.lexically_relative("c:/c/d"); // ""
fs::path{ "C:/a/b" }.lexically_relative("D:/c/d"); // ""
fs::path{ "C:/a/b" }.lexically_proximate("D:/c/d"); // "C:/a/b"
}
通用路径格式和实际平台特定实现的格式之间转换的方法。
调用 | 效果 |
---|---|
generic_string() | 返回string类型的通用路径 |
generic_wstring() | 返回wstring类型的通用路径 |
generic_u8string() | 返回u8string类型的通用路径 |
generic_u16string() | 返回u16string类型的通用路径 |
generic_u32string() | 返回u32string类型的通用路径 |
native() | 返回path::string_type 类型的本地路径格式 |
c_str() | 返回本地字符串格式的字符序列形式的路径 |
make_preferred() | 把p中的目录分隔符替换为本地格式的分隔符并返回修改后的p |
这些函数在POSIX系统上没有效果,因为这些系统的本地格式和通用格式没有区别。
调用 | 效果 |
---|---|
p = p2 | 赋予一个新路径 |
p = sv | 赋予一个字符串视图作为新路径 |
p.assign(p2/sv) | 赋予一个新路径或者字符串视图作为新路径 |
p.assign(beg,end) | 赋予从beg到end元素组成的路径 |
p1/p2 | 把p2作为子路径附加在p1之后的结果 |
p.append(sub) | 相当于p1/sub |
p.append(beg,end) | 把beg到end之间的元素作为子路径附加在p后面 |
p += str | 把str里的字符添加到路径p之后 |
p.concat(str) | 等同于p += str |
p.concat(beg,end) | 等同于p.append(beg,end) |
remove_filename() | 移除路径末尾的文件名 |
replace_filename(repl) | 替换路径末尾的文件名 |
remove_extension() | 移除路径末尾的文件的扩展名 |
replace_extension(repl) | 替换路径末尾的文件的扩展名 |
clear() | 清理路径 |
swap(p2) | 交换两个路径 |
make_preferred() | 把p中的目录分隔符替换为本地格式的分隔符并返回修改后的p |
调用 | 效果 |
---|---|
compare(p2) | 返回是小于、等于还是大于p2 |
p.compare(sv) | 返回是小于、等于还是大于字符串视图sv转换后的路径 |
equivalent(p1,p2) | 访问实际文件系统的开销较大的比较操作 |
这一节介绍开销更大的会访问实际文件系统的操作。
调用 | 效果 |
---|---|
exists§ | 返回是否存在一个可访问到的文件 |
is_symlink§ | 返回是否文件p存在并且是符号链接 |
is_regular_file§ | 文件p存在并且是普通文件 |
is_directory§ | 文件p存在并且是目录 |
is_other§ | 文件p存在并且不是普通文件或目录或符号链接 |
is_block_file§ | 文件p存在并且是块特殊文件 |
is_character_file§ | 文件p存在并且是字符特殊文件 |
is_fifo§ | 文件p存在并且是FIFO或管道文件 |
is_socket§ | 文件p存在并且是套接字 |
is_empty§ | 文件是否为空 |
file_size§ | 返回文件大小 |
hard_link_count§ | 返回硬链接数量 |
last_write_time§ | 返回最后一次修改文件的时间 |
有一个特殊的类型file_status
可以被用来存储并修改被缓存的文件类型和权限。
调用 | 效果 |
---|---|
status§ | 返回文件p的file_status(解析符号链接,并返回指向的文件的属性) |
symlink_status§ | 返回文件p的file_status(返回符号链接自身的状态) |
对file_status
的操作:
调用 | 效果 |
---|---|
exists(fs) | 文件存在 |
s_regular_file(fs) | 文件存在并且是普通文件 |
is_directory(fs) | 文件存在并且是目录 |
is_other(fs) | 文件存在并且不是普通文件或目录或符号链接 |
is_block_file(fs) | 文件存在并且是块特殊文件 |
is_character_file(fs) | 文件存在并且是字符特殊文件 |
is_fifo(fs) | 文件存在并且是FIFO或管道文件 |
is_socket(fs) | 文件存在并且是套接字 |
fs.type() | 返回文件的file_type |
fs.permissions() | 返回文件的权限 |
有一个枚举类perms
表示权限,可以通过fs.permissions
进行返回,定义如下:
enum class perms
{
none = 0, // 没有权限集
owner_read = 0400, // 所属用户只读权限
owner_write = 0200, // 所属用户只写权限
owner_exec = 0100, // 所属用户执行权限
owner_all = 0700, // 所属用户所有权限
group_read = 0040, // 所属用户组只读权限
group_write = 0020, // 所属用户组只写权限
group_exec = 0010, // 所属用户组执行权限
group_all = 0070, // 所属用户组所有权限
others_read = 0004, // 所属其他人只读权限
others_write = 0002, // 所属其他人只写权限
others_exec = 0001, // 所属其他人执行权限
others_all = 0007, // 所属其他人所有权限
all = 0777, // 所有用户所有权限
set_uid = 04000, // 运行时设置用户ID
set_gid = 02000, // 运行时设置组ID
sticky_bit = 01000, // 操作系统特定
mask = 07777, // 所有可能位的掩码
unknown = 0xFFFF, // 未知权限
_All_write = owner_write | group_write | others_write, // 所有权限只写
_File_attribute_readonly = all & ~_All_write
};
调用 | 效果 |
---|---|
create_directory§ | 创建目录 |
create_directory(p,attrPath) | 创建属性为attrpPath的目录 |
create_directories§ | 创建目录和该路径下所有不存在的目录 |
create_hard_link(to,new) | 为已经存在文件to创建一个硬链接 |
create_symlink(to,new) | 创建指向to的符号链接new |
create_directory_symlink(to,new) | 创建指向目录to的符号链接new |
copy(from,to) | 拷贝任意类型的文件 |
copy(from,to,options) | 以选项options拷贝文件 |
copy_file(from,to) | 拷贝文件(不能是目录和符号链接) |
copy_file(from,to,options) | 以选项options拷贝文件(不能是目录和符号链接) |
copy_symlink(from,to) | 拷贝符号链接(to也指向from指向的文件) |
remove§ | 删除一个文件或者空目录 |
remove_all§ | 递归删除路径p下面的所有子文件 |
options
会影响copy
操作:
copy_options | 效果 |
---|---|
none | 默认值 |
skip_existing | 跳过覆盖已有文件 |
overwrite_existing | 覆盖已有文件 |
update_existing | 如果新文件更新的话覆盖旧文件 |
recursive | 递归拷贝子目录和内容 |
copy_symlinks | 拷贝符号链接为符号链接 |
skip_symlinks | 忽略符号链接 |
directories_only | 只拷贝目录 |
create_hard_links | 创建新的硬链接而不是拷贝文件 |
create_symlinks | 创建符号链接而不是拷贝文件 |
修改已经存在的文件:
调用 | 效果 |
---|---|
rename(old,new) | 重命名或移动文件 |
last_write_time(p,newtime) | 修改最后修改时间 |
permissions(p,prms) | 更换文件权限 |
permissions(p,prms,mode) | 根据mode修改文件权限 |
resize_file(p,newSize) | 修改普通文件的大小 |
当你想处理符号链接时这些操作尤其重要。使用纯路径转换开销更小但不会访问实际的文件系统。
调用 | 效果 |
---|---|
read_symlink(symlink) | 返回符号链接指向的文件 |
absolute§ | 返回p的绝对路径(不解析符号链接) |
canonical§ | 返回已存在p的绝对路径(解析符号链接) |
weakly_canonical§ | 返回p的绝对路径(解析符号链接) |
relative§ | 返回从当前目录到p的相对路径 |
relative(p,base) | 返回从base到p的相对路径 |
proximate§ | 返回从当前目录到p的相对(或绝对)路径 |
proximate(p,base) | 返回从base到p的相对(或绝对)路径 |
以上调用,路径必须正规化。
调用 | 效果 |
---|---|
equivalent(p1,p2) | 返回p1和p2是否指向同一个文件 |
space§ | 返回路径p的磁盘信息 |
current_path§ | 将当前工作目录设置为p |
文件系统库的一个关键作用就是遍历一个文件系统树的所有文件。
最快捷的方式是使用范围for循环。
for(const auto& e : filesystem::directory_iterator(dir))
cout << e.path() << endl;
目录迭代器:
directory_iterator
recursive_directory_iterator
目录迭代器选项:
directory_options | 效果 |
---|---|
none | 默认情况 |
follow_directory_symlink | 解析符号链接,而不是跳过 |
skip_permission_denied | 当权限不足跳过目录 |
目录迭代器的元素类型是std::filesystem::directory_entry
。因此,如果目录迭代器有效的话,使用operator*()
会返回这个类型。下面是目录项的有关调用:
调用 | 效果 |
---|---|
path() | 返回当前目录项的文件系统路径 |
exists() | 返回文件是否存在 |
is_regular_file() | 返回文件是否是普通文件 |
is_directory() | 返回文件是否是目录 |
is_symlink() | 返回文件是否是符号链接 |
is_other() | 返回文件是否不是普通文件或者目录或者符号链接 |
is_block_file() | 返回文件是否是块特殊文件 |
is_character_file() | 返回文件是否是字符特殊文件 |
is_fifo() | 返回文件是否是FIFO或者管道文件 |
is_socket() | 返回文件是否是套接字 |
file_size() | 返回文件的大小 |
hard_link_count() | 返回硬链接的数量 |
last_write_time() | 返回最后一次修改时间 |
status() | 返回文件p的状态 |
symlink_status() | 返回文件p的状态(解析硬链接) |
replace_filename§ | 替换文件名并更新目录项的所有属性 |
refresh() | 更新该目录项所有缓存的属性 |
assign() | 替换对应路径并更新目录项的所有属性 |
目录项缓存:
鼓励使用缓存额外的文件属性来避免使用目录项时额外的文件系统访问开销。
因为所有的值都会被缓存,因此这些调用开销很小:
for(const auto& e : filesystem::directory_iterator("."))
cout << e.last_write_time() << endl;
在多用户或者多进程的操作系统中,所有这些迭代都可能返回不再有效的数据。文件的大小和内容可能改变、文件可能被删除或者替换、权限也可能发生改变。
for(const auto& e : filesystem::directory_iterator("."))
{
// ...
e.refresh(); // 刷新文件缓存内容
if(e.exists()) // 判断文件是否存在
cout << e.last_write_time() << endl;
}