C++17 새로운 기능들

Seungho Jeong
29 min readDec 7, 2017

--

출처: 이 글은 cpp17_in_TTs를 번역, 수정하여 작성되었습니다.

C++17에는 100가지가 넘는 변화가 있지만, 여기에서는 그 중 몇 가지를 추려서 소개합니다.

If-init

{
if (Foo * ptr = get_foo())
use(*ptr);
more_code();
}

위 코드에서 ptr은 nullptr이 반환될 수도 있으므로 if문으로 이를 검사 후 사용하는 구문이다. 포인터를 사용하기 전에 반환받은 객체가 유효한 지 검사하는 좋은 방법이지만, 아래의 예제와 같이 유효성을 검사하는 method를 실행해야 하는 경우에는 코드가 복잡해질 수 있다.

{
{
QVariant var = getAnswer();
if (var.isValid())
use(var);
}
more_code();
}

유효성 검사를 하는 isVaild() method를 if문으로 검사하고 반환받은 객체를 사용한다. 이러한 예제에서 객체를 if 조건문 안에서 할당받고 바로 유효성도 검사할 수 있도록 문법이 추가됐다.

{
if (QVariant var = getAnswer(); var.isValid())
use(var);

more_code();
}

if 조건문 안에서 var를 할당받고 초기화와 유효성 검사를 함께 수행한다. 그리고 switch문에서도 역시 사용이 가능하다.

{
switch (Device dev = get_device(); dev.state())
{
case sleep: /*...*/ break;
case ready: /*...*/ break;
case bad: /*...*/ break;
}
}

Structured Bindings

다음의 예제는 tuple의 각 element를 가져오는 C++14의 코드이다.

tuple<int, string> func();auto tup = func();
int i = get<0>(tup);
string s = get<1>(tup);

use(s, ++i);

C++14에서 또는 아래와 같이 같은 동작을 할 수 있다.

tuple<int, string> func();

int i;
string s;
std::tie(i,s) = func();

use(s, ++i);

C++17에서는 tuple의 각 element를 더 쉽게 가져오기 위해서 “[ ]”를 사용하는 새로운 문법을 지원한다.

tuple<int, string> func();

auto [ i, s ] = func();

use(s, ++i);

tuple뿐만 아니라 pair의 element에도 사용할 수 있다.

pair<int, string> func();

auto [ i, s ] = func();

use(s, ++i);

이를 compiler가 어떻게 해석하는지를 살펴보면 특기할 내용이 있다.

pair<int, string> func();

auto __tmp = func();
auto & i = get<0>(__tmp);
auto & s = get<1>(__tmp);

use(s, ++i);

위 코드에서 보면 __tmp는 copy이지만 i와 s는 reference이다. 하지만 “[ ]”를 사용하면 항상 reference로 해석되는 것은 아니며, 구조체의 member일 경우 이를 단지 치환한다. 다음의 예제를 보면 알 수 있다.

#include <string>
#include <iostream>

struct Foo
{
int x = 0;
std::string str = "world";
~Foo() { std::cout << str; }
};

int main()
{
auto [ i, s ] = Foo();
std::cout << "hello ";
s = "structured bindings";
}

위의 예제를 컴파일러가 해석하면 다음과 같다.

#include <string>
#include <iostream>

struct Foo
{
int x = 0;
std::string str = "world";
~Foo() { std::cout << str; }
};

int main()
{
auto __tmp = Foo();
std::cout << "hello ";
__tmp.str = "structured bindings";
}

출력 결과는 “ hello structured bindings”가 출력된다. 이를 통해 알 수 있는 것은 Foo의안에 있는 Foo::str을 직접 수정한다는 것이다 . 때문에 출력 시에 “world”가 아닌 “structured bindings”가 출력된다.

이러한 binding에 &를 사용하면 어떻게 동작하는 지 다음의 예제를 봐보자.

struct X { int i = 0; };
X makeX();

X x;

auto [ b ] = makeX();
b++;
auto const [ c ] = makeX();
c++;
auto & [ d ] = makeX();
d++;
auto & [ e ] = x;
e++;
auto const & [ f ] = makeX();
f++;

이 코드는 컴파일러가 다음과 같이 해석한다.

struct X { int i = 0; };
X makeX();

X x;

auto __tmp1 = makeX();
__tmp1.i++;
auto const __tmp2 = makeX();
__tmp2.i++; //error: can't modify const
auto & __tmp3 = makeX(); //error: non-const ref cannot bind to temp
auto & _tmp3 = x;
x.i++;
auto const & __tmp4 = makeX();
__tmp4.i++; //error: can't modify const

Reference로 받을 때는 항상 const로 받아야 함에 주목해야 한다. const가 아닐 경우, Reference로 받으면 원본이 수정이 될 수 있다.

컴파일러는 가능 할 경우 get<N>() method를 활용한다. 즉, 사용자가 직접 구현을 했을 경우에는 컴파일러는 이 method를 사용하여 변수의 값을 할당한다. 아래의 예제를 보면 struct일 경우와 get<N>()을 구현했을 경우의 컴파일러 동작을 확인할 수 있다.

struct Foo {
int x;
string str;
};

Foo func();
auto [ i, s ] = func();

use(s, ++i);

위 예제를 컴파일러가 해석하면 아래와 같다.

struct Foo {
int x;
string str;
};

Foo func();

Foo __tmp = func();
auto & i = __tmp.x;
auto & s = __tmp.str;

use(s, ++i);

get<N>() 구현이 없기 때문에 구조체의 element를 직접 가져온다.

class Foo {
// ...
public:
template <int N> auto & get() /*const?*/ { /*...*/ }
};
// or get outside class
template<int N> auto & get(Foo /*const?*/ & foo) { /*...*/ }
//...

// tuple_size/element specialized
// yes, in namespace std
namespace std {
// how many elements does Foo have
template<> struct tuple_size<Foo> { static const int value = 3; }
// what type is element N
template<int N> struct tuple_element<N, Foo> { using type = ...add code here...; }
}

Foo func();

auto [ i, s ] = func();

use(s, ++i);

배열에도 활용이 가능하다.

int arr[4] = { /*...*/ };
auto [ a, b, c, d ] = arr;
auto [ t, u, v ] = std::array<int,3>();

// now we're talkin'
for (auto && [key, value] : my_map)
{
//...
}

Constexpr If

기존에는 get<N>() 함수를 구현할 때, get<0>, get<1>와 같이 특정 번호를 직접 써서 만들었었다.

class Foo {
int myInt;
string myString;
public:
int const & refInt() const
{ return myInt; }
string const & refString() const
{ return myString; }
};

namespace std
{
template<> class tuple_size<Foo>
: public integral_constant<int, 2>
{ };
template<int N> class tuple_element<N, Foo>
{
public:
using type =
conditional_t<N==0,int const &,string const &>;
};
}

template<int N> std::tuple_element_t<N,Foo>
get(Foo const &);

// here's some specializations (the real stuff)
template<> std::tuple_element_t<0,Foo>
get<0>(Foo const & foo)
{
return foo.refInt();
}
template<> std::tuple_element_t<1,Foo>
get<1>(Foo const & foo)
{
return foo.refString();
}

하지만 C++17에서는 constexpr을 활용해서 구현이 가능하다.

class Foo {
int myInt;
string myString;
public:
int const & refInt() const
{ return myInt; }
string const & refString() const
{ return myString; }
};

namespace std
{
template<> class tuple_size<Foo>
: public integral_constant<int, 2>
{ };
template<int N> class tuple_element<N, Foo>
{
public:
using type =
conditional_t<N==0,int const &,string const &>;
};
}


template<int N> auto & get(Foo const & foo)
{
static_assert(0 <= N && N < 2, "Foo only has 2 members");

if constexpr (N == 0) // !! LOOK HERE !!
return foo.refInt();
else if constexpr (N == 1) // !! LOOK HERE !!
return foo.refString();
}

Deduction Guides

pair와 tuple의 사용법을 비교해보자.

pair<int, string> is1 = pair<int, string>(17, "hello");
auto is2 = std::pair<int, string>(17, "hello");
auto is3 = std::make_pair(17, string("hello"));
auto is4 = std::make_pair(17, "hello"s);

C++14까지는 위와같이 Type을 명시해야 했지만,

pair<int, string> is1 = pair(17, "hello");
auto is2 = pair(17, "hello"); // !! pair<int, char const *>
auto is3 = pair(17, string("hello"));
auto is4 = pair(17, "hello"s);

C++17에서는 “deduction guides” 기능을 활용하여 컴파일러가 Type을 추정할 수 있다. 그리고 deduction guide는 explicit과 implicit으로 기능이 나뉜다.

Explicit Deduction Guides

template<typename T>
struct Thingy
{
T t;
};

// !! LOOK HERE !!
Thingy(const char *) -> Thingy<std::string>;

Thingy thing{"A String"}; // thing.t is a `std::string`

Implicit Deduction Guides

만약에 template<typename T, typename U, etc> struct... (or class!) 에서 TU 를 받는 생성자가 있으면, 이를 이용해서 컴파일러가 Type을 추정한다. 이런 방식을 "implicit" deduction guide라고 한다. 위의 예제에서는 explicit 방식만 표현했지만, 위 예제에서 직접 구현한 부분을 implicit에서는 컴파일러가 대신 해준다.

template<auto>

C++14

template <typename T, T v>
struct integral_constant
{
static constexpr T value = v;
};
integral_constant<int, 2048>::value
integral_constant<char, 'a'>::value

C++17

template <auto v>
struct integral_constant
{
static constexpr auto value = v;
};
integral_constant<2048>::value
integral_constant<'a'>::value

Fold Expressions

다음의 예제에서 sum 함수를 C++14와 C++17은 다르게 구현 할 수 있다.

auto x = sum(5, 8);
auto y = sum(a, b, 17, 3.14, etc);

C++14

auto sum() { return 0; }

template <typename T>
auto sum(T&& t) { return t; }

template <typename T, typename... Rest>
auto sum(T&& t, Rest&&... r) {
return t + sum(std::forward<Rest>(r)...);
}

C++17

template <typename... Args>
auto sum(Args&&... args) {
return (args + ... + 0);
}

Nested Namespaces

C++14

namespace A {
namespace B {
namespace C {
struct Foo { };
//...
}
}
}

C++17

namespace A::B::C {
struct Foo { };
//...
}

Single Param static_assert

C++14

static_assert(sizeof(short) == 2, "sizeof(short) == 2")

C++17

static_assert(sizeof(short) == 2)

Output:

static assertion failure: sizeof(short) == 2

Inline Variables

C++14

// foo.h
extern int foo;

// foo.cpp
int foo = 10;
// foo.h
struct Foo {
static int foo;
};

// foo.cpp
int Foo::foo = 10;

C++17

// foo.h
inline int foo = 10;
// foo.h
struct Foo {
static inline int foo = 10;
};

Guaranteed Copy Elision

C++17

// header <mutex>
namespace std
{
template <typename M>
struct lock_guard
{
explicit lock_guard(M & mutex);
// not copyable, not movable:
lock_guard(lock_guard const & ) = delete;
//...
}
}

// your code
lock_guard<mutex> grab_lock(mutex & mtx)
{
return lock_guard<mutex>(mtx);
}

mutex mtx;

void foo()
{
auto guard = grab_lock(mtx);
/* do stuff holding lock */
}

some new [[attributes]]

[[fallthrough]]

C++14

switch (device.status())
{
case sleep:
device.wake();
// fall thru
case ready:
device.run();
break;
case bad:
handle_error();
break;
}

C++17

switch (device.status())
{
case sleep:
device.wake();
[[fallthrough]];
case ready:
device.run();
break;
case bad:
handle_error();
break;
}

C++14에서는 다음과 같은 Warning이 발생한다.

warning: case statement without break

[[nodiscard]]

함수에서의 동작:

C++14

struct SomeInts
{
bool empty();
void push_back(int);
//etc
};

void random_fill(SomeInts & container,
int min, int max, int count)
{
container.empty(); // empty it first
for (int num : gen_rand(min, max, count))
container.push_back(num);
}

C++17

struct SomeInts
{
[[nodiscard]] bool empty();
void push_back(int);
//etc
};

void random_fill(SomeInts & container,
int min, int max, int count)
{
container.empty(); // empty it first
for (int num : gen_rand(min, max, count))
container.push_back(num);
}

C++17에서는 다음과 같은 Warning이 발생한다.

warning: ignoring return value of 'bool empty()'

Class나 Struct에서의 동작:

C++14

struct MyError {
std::string message;
int code;
};

MyError divide(int a, int b) {
if (b == 0) {
return {"Division by zero", -1};
}

std::cout << (a / b) << '\n';

return {};
}

divide(1, 2);

C++17

struct [[nodiscard]] MyError {
std::string message;
int code;
};

MyError divide(int a, int b) {
if (b == 0) {
return {"Division by zero", -1};
}

std::cout << (a / b) << '\n';

return {};
}

divide(1, 2);

C++17에서는 다음과 같은 Warning이 발생한다.

warning: ignoring return value of function declared with 'nodiscard' attribute

[[maybe_unused]]

C++14

bool res = step1();
assert(res);
step2();
etc();

C++17

[[maybe_unused]] bool res = step1();
assert(res);
step2();
etc();

C++14에서는 다음과 같은 Warning이 발생한다.

warning: unused variable 'res'

함수에서도 사용이 가능하다.

[[maybe_unused]] void f()
{
/*...*/
}
int main()
{
}

std::string_view

다음과 같은 parser를 만들었다고 가정해보자.

Foo parseFoo(std::string const & input);

하지만 어떤 사용자는 const char*를 사용할 수가 있어서 다음의 코드를 추가한다.

Foo parseFoo(char const * str);

그리고 또 어떤 사용자는 문자열의 일부분만 파싱하고 싶을 수가 있어 다음의 코드가 추가된다.

Foo parseFoo(char const * str, int length);

그리고 또 커스텀 문자열 클래스가 있으면 다음을 추가해야 한다.

Foo parseFoo(MyString const & str);

이 인터페이스를 유지하기 위해서 string_view를 사용할 수 있다. 다음의 예제를 봐보자.

C++14

Foo parseFoo(std::string const & input);
Foo parseFoo(char const * str);

Foo parseFoo(char const * str, int length);




Foo parseFoo(MyString const & str);

C++17

Foo parseFoo(std::string_view input);

// I would say don't offer this interface, but:
Foo parseFoo(char const * str, int length)
{
return parseFoo(string_view(str,length));
}

class MyString {
//...
operator string_view() const
{
return string_view(this->data, this->length);
}
};

MyString과 같이 문자열 역할을 하는 클래스가 계속 추가가 되어도 string_view() operator를 overriding하면 parseFoo 함수를 추가하지 않아도 인터페이스가 유지되면서 사용할 수 있다.

std::optional<T>

만약 다음의 parseFoo 함수가 실패를 할 경우에는 어떻게 처리를 해야 할까?

Foo parseFoo(std::string_view input);
  1. exception을 던진다.
  2. default 생성자가 있을 경우 Foo를 default 상태로 반환한다.
  3. bool parseFoo(std::string_view input, Foo & output); 로 바꾼다.
  4. Foo * parseFoo(std::string_view input); // 할당을 한다? :-(

C++14

// returns default Foo on error
Foo parseFoo(std::string_view in);

// throws parse_error
Foo parseFoo(std::string_view in);

// returns false on error
bool parseFoo(std::string_view in, Foo & output);

// returns null on error
unique_ptr<Foo> parseFoo(std::string_view in);

C++17

std::optional<Foo> parseFoo(std::string_view in);

optional의 활용법은 다음과 같다.

optional ofoo = parseFoo(str);
if (ofoo)
use(*ofoo);

또는 If-init과 함께 사용이 가능하다.

// nicer with new if syntax:
if (optional ofoo = parseFoo(str); ofoo)
use(*ofoo);

그리고 정수형 변환과 같은 예제에서도 사용이 가능하다.

optional<int> oi = parseInt(str);
std::cout << oi.value_or(0);

참고로, optional은 에러처리만을 위해서 사용하지 않는다. boost의 optional과 Haskell의 Maybe를 살펴보길 바란다.

std::variant<A,B,C,…>

공용체를 더욱 쉽게 사용하기 위한 기능이다.

C++14

struct Stuff
{
union Data {
int i;
double d;
string s; // constructor/destructor???
} data;
enum Type { INT, DOUBLE, STRING } type;
};

C++17

struct Stuff
{
std::variant<int, double, string> data;

};

다음과 같이 사용한다.

C++14

void handleData(int i);
void handleData(double d);
void handleData(string const & s);

//...

switch (stuff.type)
{
case INT:
handleData(stuff.data.i);
break;
case DOUBLE:
handleData(stuff.data.d);
break;
case STRING:
handleData(stuff.data.s);
break;
}

C++17

void handleData(int i);
void handleData(double d);
void handleData(string const & s);

//...

std::visit([](auto const & val) { handleData(val); }, stuff.data);

// can also switch(stuff.data.index())

Lambda는 다음과 같이 사용한다.

struct ThatLambda
{
void operator()(int const & i) { handleData(i); }
void operator()(double const & d) { handleData(d); }
void operator()(string const & s) { handleData(s); }
};

ThatLambda thatLambda;
std::visit(thatLambda, stuff.data);

또 다른 사용예는 다음과 같다.

if (holds_alternative<int>(data))
int i = get<int>(data);

// throws if not double:
double d = get<double>(data);
std::variant<Foo, Bar> var; // calls Foo()
// (or doesn't compile if no Foo())

Bar bar = makeBar();
var = bar; // calls ~Foo() and Bar(Bar const &)
// (what if Bar(Bar const & b) throws?)

var = Foo(); // calls ~Bar() and move-ctor Foo(Foo &&)
// (what if Foo(Foo && b) throws? - even though moves shouldn't throw)

var = someFoo; // calls Foo::operator=(Foo const &)


std::variant<Foo, std::string> foostr;

foostr = "hello"; // char * isn't Foo or string
// yet foostr holds a std::string

std::any

std::variant<A,B,C> 는 A, B, C만 할당 가능하지만, std::any 는 거의 대부분을 할당할 수 있다.

C++14

void * v = ...;
if (v != nullptr) {
// hope and pray it is an int:
int i = *reinterpret_cast<int*>(v);
}

C++17

std::any v = ...;
if (v.has_value()) {
// throws if not int
int i = any_cast<int>(v);
}

C++17

std::any v = ...;
if (v.type() == typeid(int)) {
// definitely an int
int i = any_cast<int>(v);
}

참고로 std::any는 template이 아니다. 그러므로 compile time이 아닌 runtime에서 Type을 변경하는 것이 가능하다.

C++14

// can hold Circles, Squares, Triangles,...
std::vector<Shape *> shapes;

C++17

// can hold Circles, Squares, Triangles, ints, strings,...
std::vector<any> things;

namespace std::filesystem

C++14 Windows

#include <windows.h>

void copy_foobar() {
std::wstring dir = L"\\sandbox";
std::wstring p = dir + L"\\foobar.txt";
std::wstring copy = p;
copy += ".bak";
CopyFile(p, copy, false);

std::string dir_copy = dir + ".bak";
SHFILEOPSTRUCT s = { 0 };
s.hwnd = someHwndFromSomewhere;
s.wFunc = FO_COPY;
s.fFlags = FOF_SILENT;
s.pFrom = dir.c_str();
s.pTo = dir_copy.c_str();
SHFileOperation(&s);
}

void display_contents(std::wstring const & p) {
std::cout << p << "\n";

std::wstring search = p + "\\*";
WIN32_FIND_DATA ffd;
HANDLE hFind =
FindFirstFile(search.c_str(), &ffd);
if (hFind == INVALID_HANDLE_VALUE)
return;

do {
if ( ffd.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY) {
std::cout << " " << ffd.cFileName << "\n";
} else {
LARGE_INTEGER filesize;
filesize.LowPart = ffd.nFileSizeLow;
filesize.HighPart = ffd.nFileSizeHigh;
std::cout << " " << ffd.cFileName
<< " [" << filesize.QuadPart
<< " bytes]\n";
}
} while (FindNextFile(hFind, &ffd) != 0);
}

C++14 POSIX

#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>

void copy_foobar() {

// [TODO]
// to copy file, use fread / fwrite

// how to copy directory...?
}

void display_contents(std::string const & p) {
std::cout << p << "\n";

struct dirent *dp;
DIR *dfd;

if ((dfd = opendir(p.c_str()) == nullptr)
return;

while((dp = readdir(dfd)) != nullptr) {
struct stat st;
string filename = p + "/" + dp->d_Name;
if (stat(filename.c_str(), &st) == -1)
continue;

if ((st.st_mode & S_IFMT) == S_IFDIR)
std::cout << " " << filename << "\n";
} else {
std::cout << " " << filename
<< " [" << st.st_size
<< " bytes]\n";
}
}
}

C++17

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

void copy_foobar() {
fs::path dir = "/";
dir /= "sandbox";
fs::path p = dir / "foobar.txt";
fs::path copy = p;
copy += ".bak";
fs::copy(p, copy);
fs::path dir_copy = dir;
dir_copy += ".bak";
fs::copy(dir, dir_copy, fs::copy_options::recursive);
}

void display_contents(fs::path const & p) {
std::cout << p.filename() << "\n";

if (!fs::is_directory(p))
return;

for (auto const & e: fs::directory_iterator{p}) {
if (fs::is_regular_file(e.status())) {
std::cout << " " << e.path().filename()
<< " [" << fs::file_size(e) << " bytes]\n";
} else if (fs::is_directory(e.status())) {
std::cout << " " << e.path().filename() << "\n";
}
}
}

--

--