Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

28 Apr 2019

C++17的string_view

C++17标准库中新增了一个类型,std::string_view。这个类型可以看成是指向字符串的指针加上该字符串的长度的集合。和std::string相比,使用string_view可以避免一些内存分配,从而提高程序性能。但是有一点需要注意:string_view并不拥有其指针所指向的字符串,当该字符串被释放的时候,string_view有效性也就随着消失了。

string_view本身是只读的,相当于const std::string&,但是string_view中的指针以及字符长度可以随意修改(在原字符串的有效范围之内),而不会引起内存分配,同时也保证不会访问字符串有效范围之外的部分,导致内存访问错误。

一个简单的例子

#include <string_view>
#include <string>
#include <iostream>

using namespace std;

void f(string_view sv)
{
  cout << sv << endl;
}

int main()
{
  f("hello");
  return 0;
}

编译上面的代码并执行:

> c++ -std=c++17 z.cc -o z
> ./z
hello

std::string可以自动转化为std::string_view,修改main函数如下:

int main()
{
  string str = "hello";
  f(str);
  return 0;
}

编译并执行上述代码所得到的输出依然是:hello

假设我们有一个struct可以从一个string_view构造出来:

struct Foo
{
  Foo(std::string_view sv)
    : size(sv.size())
  {}

  size_t size;
};

修改main函数如下:

int main()
{
  string str = "hello";
  Foo foo(str);
  cout << str << " " << foo.size << endl;
  return 0;
}

编译执行后,输出结果:hello 5

但是我们做些许修改,把Foo foo(str);替换成Foo foo = str;:

int main()
{
  string str = "hello";
  Foo foo = str;
  cout << str << " " << foo.size << endl;
  return 0;
}

会发现居然编译出错。看来在C++中虽然这两种方式都是用来构造Foo,但是语义还是有所不同的。在Foo foo = str;这种形势下,编译器不会进行自动隐式转换。

具体出错信息如下:

z.cc:24:7: error: no viable conversion from 'std::__1::string' (aka
      'basic_string<char, char_traits<char>, allocator<char> >') to 'Foo'
  Foo foo = str;
      ^     ~~~
z.cc:12:8: note: candidate constructor (the implicit copy constructor) not
      viable: no known conversion from 'std::__1::string' (aka
      'basic_string<char, char_traits<char>, allocator<char> >') to
      'const Foo &' for 1st argument
struct Foo
       ^
z.cc:12:8: note: candidate constructor (the implicit move constructor) not
      viable: no known conversion from 'std::__1::string' (aka
      'basic_string<char, char_traits<char>, allocator<char> >') to 'Foo &&'
      for 1st argument
struct Foo
       ^
z.cc:14:3: note: candidate constructor not viable: no known conversion from
      'std::__1::string' (aka 'basic_string<char, char_traits<char>,
      allocator<char> >') to 'std::string_view' (aka
      'basic_string_view<char>') for 1st argument
  Foo(std::string_view sv)
  ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchai
n/usr/include/c++/v1/string:869:5: note: 
      candidate function
    operator __self_view() const _NOEXCEPT { return __self_view(data(...
    ^
1 error generated.

我们可以使用<type_traits>中定义的的std::is_convertible模板类来判断类型之间是否能够直接转化:

int main()
{
  string str = "hello";
  // Foo foo = str;
  // cout << str << " " << foo.size << endl;
  cout << is_convertible<string, string_view>::value << endl;
  cout << is_convertible<string_view, Foo>::value << endl;
  cout << is_convertible<string, Foo>::value << endl;
  return 0;
}

上面程序的编译输出结果是:

1
1
0

说明stringFoo之间不存在直接的转化关系。但是string_view是这两个类型的中间类型,是否有办法通过这个中间类型来搭桥呢?答案是肯定的,但是需要借助下面这个类型助手:

  • std::enable_if

增加一个Foo的模版构造函数:

  template<class T, typename enable_if<is_convertible<T, string_view>::value, int>::type = 0>
  Foo(const T& t)
  : Foo(string_view(t))
  {}

简单的解释一下上面这个模板函数,它只在T的类型可以转化为string_view的时候才生效。然后Foo(const T& t)通过Foo(string_view(t))把参数转化为string_view,所以实际调用的是Foo(string_view)这个构造函数。

再编译之前的main函数,输出就变成三个1了。

把main函数改为:

int main()
{
  string str = "hello";
  Foo foo = str;
  cout << str << " " << foo.size << endl;

  return 0;
}

编译并执行,可以看到Foo foo = str;编译通过了,输出结果为:hello 5

参考

(完)

05-12更新

在上面的例子中,也可以使用 Foo foo = {str};这种方式而不需要增加Foo的模版构造函数。

在C++11之前,Foo foo = {str};属于copy initialization,对隐式转化的限制比较严格,看下面的例子:

struct S { S(std::string) {} }; // implicitly convertible from std::string
S s("abc"); // OK: conversion from const char[4] to std::string
S s = "abc"; // Error: no conversion from const char[4] to S
S s = "abc"s; // OK: conversion from std::string to S

在C++11以及之后,Foo foo = {str};属于list initialization,对隐式转化有较高的宽容度,所以不会报错。

(更新完)

comments powered by Disqus