C++17 new feature

· 6 minute read

1. structured binding

Binds the specified names to subobjects or elements of the initializer. Like a reference, a structured binding is an alias to an existing object. Unlike a reference, a structured binding does not have to be of a reference type.

类似引用,可以绑定到已经存在的结构化对象的成员,但不一定是引用类型(即可以是copy)

语法:

auto (thread local | static) ref-qualifier [identifier-list] = exp;
auto (thread local | static) ref-qualifier [identifier-list]{exp};
auto (thread local | static) ref-qualifier [identifier-list](exp);

其中,

  • ref-qualifier: 可以选择是否是引用类型(&,&&)或拷贝
  • identifier-list: 列出被绑定的成员的别名

Case study:

1.1 bind to array

// clang -o bind_array -std=c++17 bind_array.cc
// g++ -o bind_array -std=gnu++17 bind_array.cc

#include <cstdio>


void foo_bind_array() {
  int a[2] = {1, 2};
  // NOTE. make a copy of a, and [x, y] refer to the copied temp array.
  auto [x, y] = a;
  // NOTE. [xr, yr] refers to original array `a` directly.
  auto &[xr, yr] = a;

  printf("original addr:%p\n", (void *)a);
  printf("after cp, addr:%p,%p\n", (void *)&x, (void *)&y);
  printf("after ref, addr:%p,%p\n", (void *)&xr, (void *)&yr);
  printf("x,y={%d,%d}\n", x, y);
  printf("xr,yr={%d,%d}\n", xr, yr);
}

int main() {
  foo_bind_array();

  return 0;
}

output:

original addr:0x7fffffffe2c8
after cp, addr:0x7fffffffe2d0,0x7fffffffe2d4
after ref, addr:0x7fffffffe2c8,0x7fffffffe2cc
x,y={1,2}
xr,yr={1,2}

1.2 bind to tuple

// clang -o bind_tuple -std=c++17 bind_tuple.cc
// g++ -o bind_tuple -std=gnu++17 bind_tuple.cc

#include <cstdio>

#include <tuple>


void foo_bind_tuple() {
  float x{1.23};
  char y{'x'};
  int z{996};

  printf("x:type(float), val(%f), addr(%p)\n", x, (void *)&x);
  printf("y:type(char), val(%c), addr(%p)\n", y, (void *)&y);
  printf("z:type(int), val(%d), addr(%p)\n", z, (void *)&z);

  //std::tuple<float &, char &&, int> tpl(x, std::move<char&&>(y), z);
  std::tuple<float &, char &&, int> tpl(x, std::forward<char&&>(y), z);
  const auto &[a, b, c] = tpl;

  printf("a:type(float), val(%f), addr(%p)\n", a, (void *)&a);
  printf("b:type(char), val(%c), addr(%p)\n", b, (void *)&b);
  printf("c:type(int), val(%d), addr(%p)\n", c, (void *)&c);

  // clang-format off
  // using Tpl = const std::tuple<float&, char&&, int>;
  // a names a structured binding that refers to x (initialized from get<0>(tpl))
  // decltype(a) is std::tuple_element<0, Tpl>::type, i.e. float&
  // b names a structured binding that refers to y (initialized from get<1>(tpl))
  // decltype(b) is std::tuple_element<1, Tpl>::type, i.e. char&&
  // c names a structured binding that refers to the third component of tpl, get<2>(tpl)
  // decltype(c) is std::tuple_element<2, Tpl>::type, i.e. const int
  // clang-format on
}

int main() {
  foo_bind_tuple();

  return 0;
}

output:

x:type(float), val(1.230000), addr(0x7fffffffe298)
y:type(char), val(x), addr(0x7fffffffe297)
z:type(int), val(996), addr(0x7fffffffe29c)
a:type(float), val(1.230000), addr(0x7fffffffe298)
b:type(char), val(x), addr(0x7fffffffe297)
c:type(int), val(996), addr(0x7fffffffe2c0)

1.3 bind struct data member

// clang -o bind_data_member -std=c++17 bind_data_member.cc
// g++ -o bind_data_member -std=gnu++17 bind_data_member.cc

#include <cstdio>


struct S {
  mutable int x1 : 2; // mutable data member, could be modified by const
                      // member function
  volatile double y1;

  void dump() {
    printf("x1:%d,addr:%p,  y1:%lf, addr:%p\n", x1, (void *)this, y1,
           (void *)&y1);
  }
};

void foo_bind_data_member_ref() {
  printf("\n%s\n", __func__);
  S s{1, 2.3};
  const auto &[x, y] = s; // x is an int lvalue identifying the 2-bit bit-field
                          // y is a const volatile double lvalue
  printf("Before modify:\n");
  s.dump();
  x = 0; // S.x1 is mutable data member, could be modified although [x, y] is
         // const value.
  // y = -1.23; // Error: y is const-qualified
  printf("After modify:\n");
  s.dump();
}

void foo_bind_data_member_cp() {
  printf("\n%s\n", __func__);
  S s{1, 2.3};
  const auto [x, y] = s; // x is an int lvalue identifying the 2-bit bit-field
                          // y is a const volatile double lvalue
  printf("Before modify:\n");
  s.dump();
  x = 0; // S.x1 is mutable data member, could be modified although [x, y] is
         // const value.
  // y = -1.23; // Error: y is const-qualified
  printf("After modify:\n");
  s.dump();
}

int main() {
  foo_bind_data_member_ref();
  foo_bind_data_member_cp();

  return 0;
}

output:

foo_bind_data_member_ref
Before modify:
x1:1,addr:0x7fffffffe2b0,  y1:2.300000, addr:0x7fffffffe2b8
After modify:
x1:0,addr:0x7fffffffe2b0,  y1:2.300000, addr:0x7fffffffe2b8

foo_bind_data_member_cp
Before modify:
x1:1,addr:0x7fffffffe2b0,  y1:2.300000, addr:0x7fffffffe2b8
After modify:
x1:1,addr:0x7fffffffe2b0,  y1:2.300000, addr:0x7fffffffe2b8

1.4 bind to map

// clang -o bind_map -std=c++17 -lstdc++ bind_map.cc
// g++ -o bind_map -std=gnu++17 -lstdc++ bind_map.cc

#include <cstdio>

#include <string>

#include <unordered_map>


struct Key {
  int _key;

  Key(const int k) : _key(k) {}

  struct local_hash {
    std::size_t operator()(const Key &k) const noexcept {
      return std::hash<int>{}(k._key);
    }
  };

  struct local_eq {
    bool operator()(const Key &l, const Key &r) const noexcept {
      return l._key == r._key;
    }
  };
};

struct Value {
  std::string _val;

  Value(const std::string &v) : _val(v) {}
};

typedef std::unordered_map<Key, Value, Key::local_hash, Key::local_eq> map_t;

void foo_bind_map_ref() {
  printf("\n%s\n", __func__);

  map_t _map{{Key(0), Value("0_val")},
             {Key(1), Value("1_val")},
             {Key(2), Value("2_val")}};
/*
NOTE: try_emplace 是C++17的新特性,返回一个tuple,其中第二个表示是否成功。
*/
#define insert_map(seq)                                                        \
  do {                                                                         \
    if (const auto &[iter, success] = _map.try_emplace(seq, #seq "_val");      \
        !success) {                                                            \
      printf("emplace fail\n");                                                \
    }                                                                          \
  } while (0)

  insert_map(10);
  insert_map(11);
  insert_map(12);

#undef insert_map

  for (const auto &[key, val] : _map) {
    printf("k:%d, v:%s\n", key._key, val._val.c_str());
  }
}

int main() {
  foo_bind_map_ref();

  return 0;
}

output:

foo_bind_map_ref
k:12, v:12_val
k:11, v:11_val
k:10, v:10_val
k:0, v:0_val
k:1, v:1_val
k:2, v:2_val

1.5 captured by lambda

// clang -o bind_captured_by_lambda -std=c++17 bind_captured_by_lambda.cc
// g++ -o bind_captured_by_lambda -std=gnu++17 bind_captured_by_lambda.cc

#include <cstdio>

#include <cassert>


struct S {
  int p{6}, q{7};
};

int main() {
  const auto &[b, d] = S{};
  auto l = [b, d] { return b * d; }; // valid since C++20 for both clang and g++. clang not handle this before C++20.
  // auto l = [b=b, d=d] { return b * d; }; // force passing `b,d` into lambda is ok.
  assert(l() == 42);
  return 0;
}

2. tuple implicit deduction

C++17增加了tuple类型的推导,不需要手动指定模板参数

// clang++ -o tuple_implicit_infer -std=c++17 tuple_implicit_infer.cc
// g++ -o tuple_implicit_infer -std=gnu++17 tuple_implicit_infer.cc

#include <cstdio>

#include <string>

#include <tuple>


void before_cpp17() {
  int a0 = 123;
  float a1 = 3.21;
  char a2 = 'y';
  std::string a3("tuple");

  std::tuple<int, float, char, std::string> tpl(a0, a1, a2, a3);
}

void after_cpp17() {
  int a0 = 123;
  float a1 = 3.21;
  char a2 = 'y';
  std::string a3("tuple");

  std::tuple tpl(a0, a1, a2, a3);
}

int main() {
  before_cpp17();
  after_cpp17();

  return 0;
}

3. if

3.1 if constexpr

The statement that begins with if constexpr is known as the constexpr if statement.

编译时分支判断,可以部分替代类似#if..#elif甚至用模板实现的重载等。

Case study:

// clang++ -o if_constexpr -std=c++17 if_constexpr.cc
// g++ -o if_constexpr -std=gnu++17 if_constexpr.cc

#include <type_traits>


// 通过 `std::is_pointer_v`在编译期间判断类型,选择不同分支
template<typename T>
auto get_value(T t)
{
    if constexpr (std::is_pointer_v<T>)
        return *t; // deduces return type to int for T = int*
    else
        return t;  // deduces return type to int for T = int
}

// 类似`#if true`的功能
extern int x; // no definition of x required

int f()
{
    if constexpr (true)
        return 0;
    else if (x)
        return x;
    else
        return -x;
}

void f2()
{
    if constexpr(false)
    {
        int i = 0;
        int *p = i; // Error even though in discarded statement
    }
}

// 检查模板参数数量
template<typename T, typename ... Rest>
void g(T&& p, Rest&& ...rs)
{
    // ... handle p
    if constexpr (sizeof...(rs) > 0)
        g(rs...); // never instantiated with an empty argument list.
}

// 编译期逻辑推导
template<class T>
void g2()
{
    auto lm = [=](auto p)
    {
        if constexpr (sizeof(T) == 1 && sizeof p == 1)
        {
            // this condition remains value-dependent after instantiation of g<T>,
            // which affects implicit lambda captures
            // this compound statement may be discarded only after
            // instantiation of the lambda body
        }
    };
}

// 检查是否是数值类型
template<typename T>
void f3()
{
    if constexpr (std::is_arithmetic_v<T>) {}
        // ...
    else {
        using invalid_array = int[-1]; // ill-formed: invalid for every T
        static_assert(false, "Must be arithmetic"); // ill-formed before CWG2518
    }
}

3.2 if init-statement

支持在if条件判断语句中进行临时变量初始化

std::map<int, std::string> m;
std::mutex mx;
extern bool shared_flag; // guarded by mx
 
int demo()
{
    if (auto it = m.find(10); it != m.end())
        return it->second.size();
 
    if (char buf[10]; std::fgets(buf, 10, stdin))
        m[0] += buf;
 
    if (std::lock_guard lock(mx); shared_flag)
    {
        unsafe_ping();
        shared_flag = false;
    }
 
    if (int s; int count = ReadBytesWithSignal(&s))
    {
        publish(count);
        raise(s);
    }
 
    if (const auto keywords = {"if", "for", "while"};
        std::ranges::any_of(keywords, [&tok](const char* kw) { return tok == kw; }))
    {
        std::cerr << "Token must not be a keyword\n";
    }
}

4. shared_mutex

C++11的std::mutex是独占的(即不区分读写锁),通过locktry_lock去占取mutex。C++17的shared_mutex提供更细的控制,可以被locklock_shared加锁,前者独占(write)后者共享(read)。

std::shared_mutex mutex_;
std::unique_lock lock(mutex_);// write mode
std::shared_lock lock(mutex_);// read mode

5. string_view

The class template basic_string_view describes an object that can refer to a constant contiguous sequence of CharT with the first element of the sequence at position zero. A typical implementation holds only two members: a pointer to constant CharT and a size.

std::string_view对字符串不具有所有权,成员对象有字符串指针地址和字符串size。其成员函数substr等是在做指针偏移,不涉及字符串拷贝,所以效率更高。

// clang++ -o string_view -std=c++17 string_view.cc
// g++ -o string_view -std=gnu++17 string_view.cc
#include <iostream>

#include <string>

#include <string_view>

#include <cstdio>


int main()
{
    constexpr std::string_view unicode[] { "▀▄─", "▄▀─", "▀─▄", "▄─▀" };

    for (int y{}, p{}; y != 6; ++y, p = ((p + 1) % 4))
    {
        for (int x{}; x != 16; ++x)
            std::cout << unicode[p];
        std::cout << '\n';
    }

    // substr
    const std::string org_str("123456abcdef");
    const std::string_view view_str(org_str);
    const std::string_view sub_str = view_str.substr(/*pos*/4, /*len*/4);
    printf("org str addr:%p, str size:%ld\n", (void*)org_str.data(), org_str.size());
    printf("view str addr:%p, str size:%ld\n", (void*)view_str.data(), view_str.size());
    printf("sub str addr:%p, str size:%ld\n", (void*)sub_str.data(), sub_str.size());

    return 0;
}

output:

...
org str addr:0x7fffffffe280, str size:12
view str addr:0x7fffffffe280, str size:12
sub str addr:0x7fffffffe284, str size:4

6. std::any

类似 void *的含义,但any会记录类型,在std::any_cast时做类型检查,在对象释放时根据类型进行析构。

case study:

#include <any>

#include <iostream>

 
int main()
{
    std::cout << std::boolalpha;
 
    // any type
    std::any a = 1;
    std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
    a = 3.14;
    std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n';
    a = true;
    std::cout << a.type().name() << ": " << std::any_cast<bool>(a) << '\n';
 
    // bad cast
    try
    {
        a = 1;
        std::cout << std::any_cast<float>(a) << '\n';
    }
    catch (const std::bad_any_cast& e)
    {
        std::cout << "cast int to float: " << e.what() << '\n';
    }
 
    // has value
    a = 2;
    if (a.has_value())
        std::cout << a.type().name() << ": has value " << std::any_cast<int>(a) << '\n';
 
    // reset
    a.reset();
    if (!a.has_value())
        std::cout << "no value\n";
 
    // pointer to contained data
    a = 3;
    int* i = std::any_cast<int>(&a);
    std::cout << *i << '\n';
}

output:

i: 1
d: 3.14
b: true
cast int to float: bad any_cast
i: has value 2
no value
3

7. std::optional

The class template std::optional manages an optional contained value, i.e. a value that may or may not be present.

llvm已经把llvm::optional替换成标准库的实现了. 常用于函数返回值类型。

#include <iostream>

#include <optional>

#include <string>

 
// optional can be used as the return type of a factory that may fail
std::optional<std::string> create(bool b)
{
    if (b)
        return "Godzilla";
    return {};
}
 
// std::nullopt can be used to create any (empty) std::optional
auto create2(bool b)
{
    return b ? std::optional<std::string>{"Godzilla"} : std::nullopt;
}
 
int main()
{
    auto va = create(false);
    if (!va) {
        std::cout << "check optional value through bool operator\n";
    }
    if (!va.has_value()) {
        std::cout << "check optional value through has_value member func\n";
    }
    std::cout << "create(false) returned "
              << create(false).value_or("empty") << '\n';
 
    // optional-returning factory functions are usable as conditions of while and if
    if (auto str = create2(true))
        std::cout << "create2(true) returned " << *str << '\n';
}

output:

check optional value through bool operator
check optional value through has_value member func
create(false) returned empty
create2(true) returned Godzilla

8. std::variant

The class template std::variant represents a type-safe union.

union结构体中的各个类型可选的,std::variant效果相同,多了一些helper function如holds_alternative,get。 但是union是所有成员共用内存,variant似乎是每个成员都独有内存。

#include <cassert>

#include <iostream>

#include <string>

#include <variant>

 
union UnionTy {
    int v0;
    float v1;
    double v2;
};

int main()
{
    UnionTy u;
    u.v0 = 123;
    std::cout << "size of UnionTy: " << sizeof(u) << "\n";
    std::variant<int, float, double> v, w;
    std::cout << "size of variant<int, float, double>: " << sizeof(v) << "\n";
    v = 42; // v contains int
    int i = std::get<int>(v);
    assert(42 == i); // succeeds
    w = std::get<int>(v);
    w = std::get<0>(v); // same effect as the previous line
    w = v; // same effect as the previous line
 
//  std::get<double>(v); // error: no double in [int, float]
//  std::get<3>(v);      // error: valid index values are 0 and 1
 
    try
    {
        std::get<float>(w); // w contains int, not float: will throw
    }
    catch (const std::bad_variant_access& ex)
    {
        std::cout << ex.what() << '\n';
    }
 
    using namespace std::literals;
 
    std::variant<std::string> x("abc");
    // converting constructors work when unambiguous
    x = "def"; // converting assignment also works when unambiguous
 
    std::variant<std::string, void const*> y("abc");
    // casts to void const * when passed a char const *
    assert(std::holds_alternative<void const*>(y)); // succeeds
    y = "xyz"s;// set y contains std::string
    assert(std::holds_alternative<std::string>(y)); // succeeds

}

output:

size of UnionTy: 8
size of variant<int, float, double>: 16
std::get: wrong index for variant
C++