C++17新特性(三)新的标准库组件

1. optional

在编程时,我们经常会遇到可能会返回/传递/使用一个确定类型对象的场景。也就是说,这个对象可能有一个确定类型的值也可能没有任何值。因此,我们需要一种方法来模拟类似指针的语义:通过nullptr表示指针为空。解决方法就是定义该对象的同时再定义一个附加的bool类型来标志该对象有没有值。std::optional就是提供了一种类型安全的方式来实现。

1.1 使用optional
1.1.1 可选的返回值
#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;
	}
}
1.1.2 可选的参数和数据成员

另一个例子是传递可选的参数或者设置可选的数据成员:

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("");
1.2 optional类型和操作
1.2.1 optional类型

optional类

template <class T>
class optional
{};

还定义了这些类型和对象:

  • nullopt_t类型的nullopt,表示可选对象无值。
  • bad_optional_access异常类,当无值的时候访问会抛出该异常。
1.2.2 optional的操作
操作符 效果
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
}
1.3 特殊情况

一些特定的可选类型可能会导致特殊或意料之外的行为。

1.3.1 bool类型或原生指针的可选对象
optional<bool> ob{false}; // false
if(!ob) // false 
if(ob == false) // true

optional<int*> op{nullptr};
if(!op) // false
if(op == nullptr) // true
1.3.2 可选对象的可选对象

理论上,你可以定义可选对象的可选对象:

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; // 外层可选对象无值
}

2. variant

variant是C++标准库提供的一个新的联合类型,它最大的优势是提供了一种新的具有多态性的处理异质集合的方法。也就是说,它可以帮助我们处理不同类型的数据,并且不需要公共基类和指针。

2.1 variant的动机

起源于C,C++也提供了union的支持,它的作用是持有一个值,这个值的类型可能是指定的若干类型中的任意一个。然而,这项语言特性有一些缺陷:

  • 对象不知道它们现在持有的值的类型。
  • 因此,你不能持有非平凡类型,例如:string
  • 你不能从union派生。

通过variant,C++标准库提供了一种可辨识的联合。

  • 当前值的类型可知。
  • 可以持有任何类型的值。
  • 可以从它派生。

事实上,variant持有的值有若干候选项,这些选项通常有不同的类型。然而,两个不同选项的类型也有可能相同,这在多个类型相同的选项分别代表不同的含义是很有用。

variant的内存大小等于所有可能的底层类型中最大的再加上一个记录当前选项的固定内存开销。

2.2 使用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;
2.3 variant的类型和操作
2.3.1 variant的类型
template<typename... Types>
class variant;

此外,还定义了下面的类型和对象:

  • 类模板variant_size
  • 类模板variant_alternative
  • variant_npos
  • 类型monostate
  • 异常类bad_variant_access
2.3.2 variant的操作
操作符 效果
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);

}
2.3.3 访问器

另一个处理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);
}
2.3.4 异常造成的无值

如果你赋给一个variant新值时发送了异常,那么这个variant可能会进入一个非常特殊的状态,失去了旧值并且没有获取新的值。

如果遇到这种情况,那么:

  • var.valueless_by_exception()会返回true
  • var.index()会返回variant_npos
2.4 使用variant实现多态的异质集合
2.4.1 实现几何对象
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不是虚函数。

2.4.2 使用variant实现其他异质集合
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);
	}
}
2.4.3 比较多态的variant

总结一下使用variant实现的异构集合的优点和缺点:

优点:

  • 可以使用任意类型并且这些类型不需要公共的基类。
  • 你不需要使用指针来完成异质集合。
  • 不需要virtual成员函数。
  • 值语义(不会发生内存泄漏问题)。
  • vector是连续存放的。

缺点:

  • 必须在编译器指定所有可能的类型。
  • 每个元素的大小都是所有类型中最大的。
  • 拷贝元素的开销可能很大。
2.5 variant的特殊情况
2.5.1 同时有bool和string选项

如果一个variantboolstring选项,赋予一个字符串字面量可能会导致令人惊奇的事,因为字符串字面量会优先转换为bool,而不是string。例如:

variant<bool,string> v;
v = "hi";
cout << "index: " << v.index() << endl; // 0

可以使用以下的方式解决:

v.emplace<1>("hello");
v.emplace<string>("hello");

3. any

一般来说,C++是一门类型绑定和类型安全的语言。值对象被声明为确定的类型,这个类型定义了所有可能的操作、也定义了对象的行为。而且,对象不能改变自身的类型。

std::any是一种在保证类型安全的基础上还能改变自身类型的值类型。也就是说,它可以持有任意类型的值,并且知道自己当前的值是什么类型的。

实现的关键在于std::any对象内包含了值和值的类型。

对于any对象,如果你赋值一个字符串,它将会分配内存并拷贝字符串,并且存储记录当前的值为一个字符串。之后,可以使用运行时检查来判断当前的值类型。为了将当前的值转换为真实的类型,必须使用any_cast<>

3.1 使用any
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()判断是否有值。

4. byte

C++17,引入了一个类型来代表内存的最小单位:字节。byte本质上代表一个字节的值,不能进行数字或字符的操作,这样会更加类型安全。

4.1 使用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;
}
4.2 byte的类型和操作
4.2.1 byte的类型
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);
}
4.2.2 byte的操作
操作 效果
位运算(<<,>>,|,&,^) 字节的位运算
比较操作 字节的比较
to_integer() 可以把字节转换为基本类型
sizeof() 1

5. 字符串视图

C++17中,C++标准库引入了一个特殊的字符串类string_view,它能够让我们像处理字符串一样处理字符串序列,不需要为它们分配空间。也就是说string_view类型的对象只是引用一个外部的字符串序列,不需要持有它们。

5.1 和string的不同之处

string相比,string_view对象有如下特点:

  • 底层的字符序列是只读的。没有操作可以修改底层的字符。你只能赋予一个新的值、交换值、把视图缩小字符序列的子序列。
  • 字符序列不保证有空字符终止。因此,字符串视图并不是一个空字符终止的字节流。
  • data()返回的可能是nullptr
  • 没有分配器支持。
5.2 使用字符串视图

字符串视图有两个主要的应用:

  • 你可能已经分配或者映射了字符序列或者字符串的数据,并且想在不分配更多内存的情况下使用这些数据。典型的例子是内存映射文件或者处理长文本的子串。
  • 你可能想提升接收字符串为参数并以只读方式使用他们的函数/操作的性能,且这些函数不需要结尾有空字符。
5.3 使用字符串视图作为参数
#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_viewstring比起来,可能会减少一次分配堆内存的调用。

5.3.1 字符串视图有害的一面

通常智能指针会比相应的语言特性更为安全。因此,你可能会认为字符串视图比字符串引用更为安全,然而,事实上,字符串视图和原生字符指针一样危险。

  • 不要把临时字符串赋值给字符串视图。

    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成员。
  • 不要把string设为string view调用链终点。
  • 不要返回string_view。
  • 函数模板永远不应该返回泛型参数的类型T。
  • 永远不要用返回值来初始化string_view。
5.4 字符串视图的类型和操作
5.4.1 字符串视图的具体类型

在头文件中,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>;
5.4.2 字符串视图的操作
操作 效果
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
}
5.5 在API中使用字符串视图

字符串视图开销很小并且每一个string都可以用作字符串视图。但是,只有当函数按照如下约束使用参数时,字符串视图才有意义:

  • 它并不需要结尾有空字符。给一个以单个const char*为参数而没有长度参数的C函数就不属于这种情况。
  • 它不会违反传入参数的生命周期。通常,意味着接受函数之后在传入值的生命周期结束前使用它。
  • 调用者不应该更改底层字符的所有权。
  • 它可以处理参数为nullptr的情况。
5.5.1 使用字符串视图代替string
// 带前缀输出时间点
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 }; // 字符串视图没有重载+
}

6. 文件系统库

C++17,Boost.Filesytem终于被C++标准所采纳,还进行了很多调整和改进。

6.1 基本的示例
6.1.1 打印文件系统路径类的属性
#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";
	}
}
6.1.2 用switch来处理不同的文件系统类型
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
};
6.1.3 创建不同类型的文件
#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()可以去掉绝对路径的"./"
	}
}
6.2 原则和术语
6.2.1 通用的可移植的分隔符

C++标准库不仅标准化了所有的操作系统的文件系统中的公共部分,在很多情况下,C++标准还尽可能的遵循POSIX的标准的要求来实现。比如:

  • 特殊的字符不能被用作文件名。
  • 创建了文件系统不支持的元素。

不同的文件系统的差异也有个纳入考虑:

  • 大小写敏感。
  • 绝对路径和相对路径。
6.2.2 命名空间

通常文件系统是标准命名空间下的filesystem子命名空间。可以使用namespace fs = std::filesystem作为缩写。

6.2.3 文件系统路径

文件系统库的一个关键元素是path。它代表文件系统中某一个文件的位置。它由可选的根名称、可选的根目录和一些以目录分隔符分隔的文件名组成。路径可以是相对的也可以是绝对的。

路径可能有不同的格式:

  • 通用格式,可以移植。
  • 本地格式,根据底层文件系统特定的。

一些特殊的文件名:

  • .代表当前路径。
  • ..代表父目录。
6.2.4 正规化

路径可以正规化,在正规化的路径中:

  • 文件名由单个推荐的目录分隔符分隔。
  • 除非整个路径就是.,否则路径不会使用.
  • 路径中除了开头部分以外的地方不会包含..
  • 除非整个路径就是.或者..,否则路径结尾的文件名是目录时要加上目录分隔符。
6.2.5 成员函数vs独立函数

文件系统提供了一些函数,有些是成员函数有些是独立函数,这么做的目的是:

  • 成员函数开销较小。不需要进行系统调用,例如:

    mypath.is_absolute() ; // 检查路径是否是绝对的
    
  • 独立函数开销较大。因为会访问实际的文件系统,意味着要进行系统调用。

    equivalent(path1,path2); // 如果两个路径指向同一个文件返回true
    
6.2.6 错误处理

文件系统是错误的根源。你必须考虑相应的文件是否存在、文件操作是否被允许、该操作是否会违背资源限制。另外,当程序运行时其他进程可能创建、修改、或者移除了某些文件,意味着事先检查并不能保证没有错误。

文件系统使用了混合的异常处理方式:

  • 默认情况下,文件系统错误会作为异常处理。
  • 传递额外的输出参数时,可能会得到一个错误码或者错误信息,而不是异常。

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) // 文件只能只读
{
	// ...
}
6.2.7 文件类型

不同的操作系统支持不同的文件类型。它定义了一个枚举类型file_type,标准定义了如下的值。

含义
regular 普通文件
directory 目录文件
symlink 符号链接文件
character 字符特殊文件
block 块特殊文件
fifo FIFO或管道文件
socket 套接字文件
none 文件类型未知
unknown 文件存在但推断不出类型
not_found 文件不存在
6.3 路径操作

有很多处理文件系统的操作。这些操作涉及到的关键类型都是std::filesystem::path,它表示一个可能存在也可能不存在的文件的绝对或相对路径。

6.3.1 创建路径
调用 效果
path{charseq} 用一个字符序列初始化路径
path{beg,end} 用一个范围初始化路径
u8path(u8string) 用一个UTF-8字符串初始化路径
current_path() 返回当前工作目录的路径
temp_directory_path() 返回临时文件的路径
6.3.2 检查路径

路径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() 返回路径元素的终点
6.3.3 遍历路径

你可以遍历一个路径,这将会返回路径的所有元素:根名称、根目录、所有的文件名。

路径迭代器是双向迭代器。迭代器的值的类型是path

打印路径:

void printPath(const filesystem::path& p)
{
	cout << "path elements of\"" << p.string() << "\":\n";
	for (filesystem::path elem : p)
	{
		cout << " \"" << elem.string() << "\"";
	}
	cout << endl;
}
6.3.4 路径IO和转换
调用 效果
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"
}
6.3.5 本地和通用格式的转换

通用路径格式和实际平台特定实现的格式之间转换的方法。

调用 效果
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系统上没有效果,因为这些系统的本地格式和通用格式没有区别。

6.3.6 修改路径
调用 效果
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
6.3.7 比较路径
调用 效果
compare(p2) 返回是小于、等于还是大于p2
p.compare(sv) 返回是小于、等于还是大于字符串视图sv转换后的路径
equivalent(p1,p2) 访问实际文件系统的开销较大的比较操作
6.4 文件系统操作

这一节介绍开销更大的会访问实际文件系统的操作。

6.4.1 文件属性
调用 效果
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§ 返回最后一次修改文件的时间
6.4.2 文件状态

有一个特殊的类型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() 返回文件的权限
6.4.3 权限

有一个枚举类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
};
6.4.4 修改文件系统
调用 效果
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) 修改普通文件的大小
6.4.5 符号链接和依赖文件系统的路径转换

当你想处理符号链接时这些操作尤其重要。使用纯路径转换开销更小但不会访问实际的文件系统。

调用 效果
read_symlink(symlink) 返回符号链接指向的文件
absolute§ 返回p的绝对路径(不解析符号链接)
canonical§ 返回已存在p的绝对路径(解析符号链接)
weakly_canonical§ 返回p的绝对路径(解析符号链接)
relative§ 返回从当前目录到p的相对路径
relative(p,base) 返回从base到p的相对路径
proximate§ 返回从当前目录到p的相对(或绝对)路径
proximate(p,base) 返回从base到p的相对(或绝对)路径

以上调用,路径必须正规化。

6.4.6 其他文件系统操作
调用 效果
equivalent(p1,p2) 返回p1和p2是否指向同一个文件
space§ 返回路径p的磁盘信息
current_path§ 将当前工作目录设置为p
6.5 遍历目录

文件系统库的一个关键作用就是遍历一个文件系统树的所有文件。

最快捷的方式是使用范围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 当权限不足跳过目录
6.5.1 目录项

目录迭代器的元素类型是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;
}

你可能感兴趣的:(C++,c++,开发语言)