티스토리 뷰

※ 주의 사항 ※

  • 이 글의 목적은 '지식의 전달'이 아닌 '학습의 기록'입니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

C++17에서 신택스 슈거(syntactic sugar)자동화 타입 추론이 결합되어 새로운 구조체 형태의 바인딩 기능이 추가되었습니다. 신택스 슈거란 무엇이고, 자동화 타입 추론은 또 무엇인지 먼저 정리하고 넘어가도록 하겠습니다.

 

신택스 슈거(syntactic sugar)

신택스 슈거란 '사람이 쉽게 이해하고 쉽게 표현할 수 있도록 컴퓨터 언어를 디자인해 놓은 문맥'을 의미합니다. 사람이 사용하기 달콤하다는 것에서 유래되었고, 깔끔하고 명확하게 표현 가능하다는 특징이 있습니다. 

 

몇몇 예시를 들면 다음과 같습니다.

*(arr + i) -> arr[i]
(*a).x -> a->x

 

위 예시를 보면 왼쪽의 경우보다 오른쪽의 경우가 확실히 직관적이고 이해가 빠르다는 것을 느낄 수 있을 것입니다.

 

 

자동화 타입 추론

자동화 타입 추론의 개념은 C++11에서부터 생긴 것으로 보입니다. 이를 위해서는 auto 키워드를 사용합니다. 다음의 코드를 보겠습니다.

double num = 3.14;

 

우리는 위의 코드에서 3.14라는 숫자를 보고 num가 실수이며 double과 같은 자료형을 가질 것임을 쉽게 알 수 있습니다(float 형이 사용될 수도 있지만). 그렇다면 우리는 굳이 num의 타입으로 double형을 명시해주지 않아도 될 것입니다. 그리고 이를 위해서 auto 키워드가 사용됩니다.

 

auto 키워드는 선언된 변수의 초기화 식을 이용해 해당 형식을 추론하도록 컴파일러에 지시합니다. 즉, auto 키워드를 사용하여 변수를 선언하면 초기화 식에 맞춰 해당 변수의 타입이 자동으로 결정됩니다. 다음의 코드를 보겠습니다.

auto num1 = 5.6;
auto num2 = 1 + 2;
auto str = "hello";

 

위 코드를 보면 num1은 double, num2는 int, str은 const char*와 같은 자료형을 가질 것임을 쉽게 알 수 있습니다.

 

그런데 auto 키워드를 사용할 때엔 주의해야 할 사항이 있습니다. auto 키워드는 변수의 선언과 동시에 초기화될 때에만 사용할 수 있습니다. 즉, 다음과 같은 예는 사용이 불가능합니다.

//사용 불가
auto num1;
num2 = 3.14;

//사용 가능
auto num2 = 3.14;

 

그리고 다음과 같이 함수의 매개 변수에도 사용할 수 없습니다. 

void print(auto x, auto y)
{
	std::cout << x + y << endl;
}

 

단, C++14에서 auto 키워드가 함수의 반환 타입을 자동으로 추론할 수 있도록 확장됨에 따라 다음과 같이 반환 타입으로 사용될 수는 있습니다.

auto add(int x, int y)
{
	return x + y;
}

 

구조체 형태의 바인딩

구조체는 안에 여러 개의 변수들을 멤버 변수로서 두고 있습니다. 이들을 초기화하기 위해 구조체 형태의 바인딩을 적용하면 코드가 간결해지고 직관적이게 됩니다. 먼저 C++17 이전에는 이를 어떻게 처리했는지 살펴보고, 이후 C++17에서는 어떻게 처리하는지 여러 예제를 통해 살펴보겠습니다.

 

먼저 C++17 이전의 처리 방식입니다. 두 수를 입력받아 나눗셈을 수행하고, 몫과 나머지를 pair의 형태로 반환하는 함수를 다음과 같이 정의했습니다.

std::pair<int, int> divideRemainder(int dividend, int divisor)
{
	return std::pair<int, int>(dividend / divisor, dividend % divisor);
}

 

위 함수의 결과로 나오는 pair 변수의 각 값에 접근하기 위해서는 다음과 같은 방법이 사용되었습니다.

#include <iostream>

std::pair<int, int> divideRemainder(int dividend, int divisor)
{
	return std::pair<int, int>(dividend / divisor, dividend % divisor);
}

int main()
{
	const std::pair<int, int> result(divideRemainder(17, 3));
	std::cout << "17 / 3 is " << result.first 
		<< " with a remainder of " << result.second << std::endl;

	return 0;
}

/*
실행 결과

17 / 3 is 5 with a remainder of 2

*/

 

같은 기능을 C++17에서 추가된 구조체 형태의 바인딩을 이용해 구현해 보겠습니다.

#include <iostream>

std::pair<int, int> divideRemainder(int dividend, int divisor)
{
	return std::pair<int, int>(dividend / divisor, dividend % divisor);
}

int main()
{
	const auto [fraction, remainder] (divideRemainder(22, 3));
	std::cout << "22 / 3 is " << fraction
		<< " with a remainder of " << remainder << std::endl;

	return 0;
}

/*
실행 결과

22 / 3 is 7 with a remainder of 1

*/

 

차이점이 보이나요? C++17 이전에는 pair의 각 요소에 접근하기 위해 result.first나 result.second와 같은 문법을 사용해야 했습니다. 그리고 first와 second가 각각 무슨 데이터를 의미하는 것인지도 쉽게 알 수 없습니다. 하지만 C++17 이후의 예제를 보면 pair의 각 요소들이 이름을 갖게 되었고, 그 이름을 통해 쉽게 접근할 수 있음을 알 수 있습니다.

 

이런 형태의 바인딩은 std::tuple에도 동일하게 적용됩니다. 두 수를 입력받아 나눗셈을 수행하고, 몫, 나누는 수, 나머지 세 개의 데이터를 tuple의 형태로 반환하는 함수를 다음과 같이 정의했습니다.

std::tuple<int, int, int> divideRemainder(int dividend, int divisor)
{
	return std::tuple<int, int, int>(dividend / divisor, divisor, dividend % divisor);
}

 

이 함수의 결괏값을 구조체 형태의 바인딩을 이용해 출력하는 예제를 다음과 같이 작성할 수 있습니다.

#include <iostream>

std::tuple<int, int, int> divideRemainder(int dividend, int divisor)
{
	return std::tuple<int, int, int>(dividend / divisor, divisor, dividend % divisor);
}

int main()
{
	auto [fraction, divisor, remainder] = divideRemainder(27, 4);
	printf("%d, %d, %d\n", fraction, divisor, remainder);

	return 0;
}

/*
실행 결과

6, 4, 3

*/

 

또한 구조체 형태의 바인딩은 사용자 정의 구조체와도 함께 동작합니다. 다음과 같은 형태의 구조체가 있다고 가정해보겠습니다.

typedef struct 
{
	int id;
	int salary;
	std::string name;
	std::string role;
} Employee;

 

이제 구조체 형태의 바인딩을 사용하여 위 구조체의 멤버들에 접근할 수 있습니다. 다음과 같이 vector가 있는 경우 반복문에서도 처리할 수 있습니다.

#include <iostream>
#include <vector>

typedef struct 
{
	int id;
	int salary;
	std::string name;
	std::string role;
} Employee;

int main()
{
	std::vector<Employee> employees = { 
		Employee{123, 12000, std::string("koey"), std::string("junior")},
		Employee{456, 10000, std::string("tamparin"), std::string("senior")},
		Employee{789, 24000, std::string("rodajoo"), std::string("manager")},
	};

	for (const auto& [id, salary, name, role] : employees)
	{
		std::cout << "name : " << name
			<< ", role : " << role
			<< ", salary : " << salary << std::endl;
	}
}

/*
실행 결과

name : koey, role : junior, salary : 12000
name : tamparin, role : senior, salary : 10000
name : rodajoo, role : manager, salary : 24000

*/

 

지금까지의 예제를 보고 눈치챘을 수도 있습니다. 구조체 형태의 바인딩은 항상 같은 패턴이 적용됩니다. 그 패턴은 다음과 같습니다.

auto [var1, var2, ...] = <pair, tuple, struct 또는 array 표현식>;

 

  • 변수 var1, var2, ...의 인수 목록은 할당되는 표현식에 포함되는 변수의 개수와 위치가 정확하게 일치해야 한다.
  • <pair, tuple, struct 또는 array 표현식>은 다음 중 하나여야 한다.
    • std::pair
    • std::tuple
    • struct로, 모든 멤버 변수는 비정적이며, 첫 번째로 정의한 멤버 변수는 첫 번째 변수에 할당되고, 두 번째로 정의한 멤버 변수는 두 번째 변수에 할당된다.
    • 고정 크기의 배열
  • 자료형은 auto, const auto, const auto&나 auto&&도 될 수 있다.

 

꺾쇠괄호 안에 필요 이상으로 많거나 적은 변수를 작성하면 컴파일러는 오류를 발생해 문제점을 알려줍니다.

std::tuple<int, float, long> tup{1, 2.0, 3};
auto[a, b] = tup;    //컴파일 에러 발생

 

STL의 기본 데이터 구조체 상당수는 별다른 변경 없이 구조체 형태의 바인딩으로 곧바로 접근할 수 있습니다. 다음은 반복문을 이용해 std::map의 모든 구성 요소를 출력하는 예제입니다.

#include <iostream>
#include <map>

int main()
{
	std::map<std::string, size_t> animalPopulation = {
		{"humans", 7000000000},
		{"chickens", 17863376000},
		{"camels", 24246291},
		{"sheep", 1086881528}
	};

	for (const auto& [species, count] : animalPopulation)
	{
		std::cout << "There are " << count << ' ' << species << " on this planet." << std::endl;
	}
}

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함