그래프 기초

 

그래프란? 

현실 세계의 사물이나 추상적인 개념 간의 연결 관계를 표현한 것을 말한다.

그래프의 구성요소는 두 가지이며 다음과 같다.

 정점(Vertex) : 데이터를 표현한다.

간선(Edge) : 정점을 연결하는 데 사용한다.

 

오늘은 C++을 이용해서 그래프를 간단하게 구현하는 실습을 해보겠다.

 


 

우선 말로만 그래프를 표현하려고 해도 한계가 있으니 이미지를 확인해보자

 

0. 그래프 개념 이미지

보는 바와 같이 원이 정점이며 원 사이에 화살표가 간선이다.

각각의 원들이 다른 원을 가리키고 있는 것을 알 수 있다.

이제 이것을 구현해보자

 

 

1-0. 그래프 구현 실습1

우선 CreateGraph 함수를 만들고 메인에서 해당 함수를 호출해서 그래프를 구현하는 동작을 하게 할 것이다.

함수 내에 정점 Vertex를 만들고 Vertex 내부에는 다른 Vertex를 가리키는 간선을 만들었다.

하나의 Vertex에서 여러개의 간선을 가질 수 있으므로 벡터로 만들어서 관리하는 모습이다.

 

 

1-1. 그래프 구현 실습1

이제 Vertex를 선언하고 6개를 만들어주었다. 마찬가지로 여러 개의 Vertex를 벡터로 관리하는 모습이다.

그래프 개념 이미지를 참조하여 각각의 Vertex가 누구를 가리키고 있는지 간선에 저장하고 있다.

좀 더 자세히 말하면 0번 Vertex의 경우 1번과 3번 Vertex를 가리키고 있으므로 0번 Vertex 내부의

간선에 push_back을 이용해 1번과 3번 Vertex 주소를 넣고 있다.

 

 

1-2. 그래프 구현 실습1

주석의 질문을 해결하기 위한 코드이다. bool 값을 하나 만들고, 반복문에 0번 Vertex가 가지고 있는 간선을 가지고 와서

임시 변수인 edge에 넣어준 후 if문을 통해 조건에 맞는지 확인하고 있다.

조건에 맞다면 0번 Vertex에 3번 Vertex가 연결되어 있다는 뜻이므로 bool 값을 true로 바꿔준다.

 


이번에는 다른 방법을 통해 그래프를 구현해보도록 하겠다.

 

2-0. 그래프 구현 실습2

기존에는 Vertex안에 간선을 가지고 있는 형태였다면 이번에는 간선을 밖에서 관리하는 형태로 만들어보겠다.

 

 

2-1. 그래프 구현 실습2

이중 벡터, 즉 이중 배열을 만들고 6개의 정점을 만든다.

n번째 정점에 이중 배열을 이용해서 간선을 넣어주는 형태이다.

 

 

2-2. 그래프 구현 실습2

0번째 정점에 있는 간선을 꺼내와서 원하는 정점과 연결되어 있는지 확인하는 반복문이다.

 


각각의 구현 방법들이 상황에 따라 효율적인 상황이 다르기 때문에 잘 사용해야 한다.

이제 마지막으로 한 번 더 그래프를 다르게 구현해보도록 하겠다.

 

3-0. 그래프 구현 실습3

2-1 이미지처럼 연결된 간선을 따로 관리하지만 이번에는 6x6 크기의 2차원 배열을 통째로 만들고

기본 값을 false로 세팅한다. 그리고 연결된 간선만 true로 바꿔주면 된다. 

이것의 특징은 메모리 소모가 심하지만 빠른 접근이 가능하다. 그래서 간선이 많은 경우 효율적이다.

 

 

3-1. 그래프 구현 실습3

빠른 접근이 가능하기 때문에 특정 정점이 연결되어 있는지 확인하는 것이 매우 쉽다.

 

스택과 큐

 

stack은 LIFO(Last-In-First-Out, 후입 선출)의 특징을 가지고 있다.

대표적인 사용 예시로는 프로그래머들이 애용하는 Ctrl+Z가 있다. 

기본적으로 동적 배열이나 리스트의 동작 방식과 매우 유사하다. push, pop, front, empty, size 등의

기능들을 사용할 수 있다.

 


 

queue는 FIFO(First-In-First-Out, 선입선출)의 특징을 가지고 있다.

대표적으로 은행 대기열이나 다른 업종의 대기열, 게임내의 매치메이킹 등등에 사용된다.

스택보다 실전에서 자주 사용되는 것이 큐이다.

스택과 마찬가지로 push, pop, size, front, empty 등의 기능들을 사용할 수 있다.

 

 

[선형 구조 기초] 배열, 동적 배열, 연결 리스트

 

기초적인 선형 자료 구조를 복습해보자.

선형 구조는 알다시피 데이터를 순차적으로 나열한 상태를 말한다.

반대로 비선형 구조는 하나의 데이터 뒤에 다수의 자료가 올 수 있는 형태를 말한다.

 

선형 구조의 대표적인 기법

- 배열

- 연결 리스트

- 스택 / 큐

 

비선형 구조의 대표적인 기법

- 트리

- 그래프

 


 

배열

 

배열은 고정된 메모리 공간을 연속적으로 할당받아서 사용하는 선형 자료 구조이다.

고정된 공간이기 때문에 새로운 공간을 추가하거나 축소하는 등의 행동을 할 수가 없다.

그리고 무조건 연속된 공간에 할당되어야 한다는 특징이 있다.

 


 

동적 배열

 

배열을 개선한 것이 동적 배열이다.

일반 배열과 다르게 고정된 메모리 공간을 사용하지 않는다. 유동적으로 메모리 공간을 할당받아 사용할 수 있다.

일반 배열과 마찬가지로 할당받는 공간이 연속적이어야 한다. 연속적으로 할당받은 메모리 공간에 새로운 데이터를 

넣으려고 하는데 해당 공간이 부족하다면 다른 메모리 공간으로 이동해서 새롭게 할당을 받는 형식이다.

 

이러한 동적 배열은 메모리 공간이 부족하다면 계속 다른 공간으로 이동해야 하기 때문에 어찌 보면 비효율적이다.

하지만 다음과 같은 방법으로 해당 문제를 어느 정도 보완하고 있다. 바로 동적 배열 할당 정책이다.

특별한 게 있는 것은 아니고 실제로 사용할 공간보다 여유롭게(약 1.5~2배) 공간을 할당받는 방식이다.

매번 비효율적으로 메모리 공간을 이동하는 것이 비효율적이기 때문에 이러한 것을 최소화하기 위한 방법이다.

 

그럼 동적 배열은 단점이 없을까?

단점은 중간 삽입/삭제가 매우 비효율적이라는 것이다.

이 단점은 따로 대책이 없기 때문에 동적 배열을 사용하고 있다면 웬만하면 중간 삽입/삭제는 하지 않는 것을 권장한다.

 


 

연결 리스트

 

연결 리스트의 경우 연속되지 않은 메모리 할당 공간을 사용하는 특징이 있다.

각 공간들이 서로의 위치 정보를 가지고 있어서 이 위치 정보를 통해 다음 저장 공간으로 이동해서 데이터를

사용하는 방식이다. 그렇기 때문에 특정 공간의 데이터를 사용하고 싶을 때, 즉 중간 삽입/삭제에 이점이 있다.

데이터들이 연속된 공간에 할당되어 있는 것이 아니기 때문에 각 저장 공간에 있는 위치 정보만 수정하게 되면

중간에 삽입이나 삭제가 매우 간편하다. 

 

하지만 단점으로는 n번째 저장 공간을 빠르게 찾을 수 없다는 점이다. 다시 말해 임의 접근이 매우 비효율적이다.

 

그렇다면 여기서 모순점이 생긴다.

임의 접근이 좋지 않은데 중간 삽입/삭제는 왜 장점일까?

중간에 데이터를 삽입/삭제하기 위해서는 특정 지점의 위치를 알아야 할 텐데 그렇다면 임의 접근을 해야 한다는

소리 아닌가? 라고 생각할 수 있다. 맞는 말이긴 하지만 다음과 같은 방법으로 중간 삽입/삭제를 하게 된다.

 

특별한 방법은 아니고 특정 지점의 위치 정보를 따로 관리하는 것이다.

연결 리스트가 임의 접근이 비효율적이기 때문에 어느 공간으로 이동해야 할지 위치 정보를 따로 관리하게 되면

매우 효율적으로 특정 지점으로 이동할 수 있게 된다. 

이러한 관점에서 중간 삽입/삭제가 장점이라는 소리다.

'자료구조와 알고리즘' 카테고리의 다른 글

그래프 기초  (0) 2022.07.28
스택과 큐 개념 복습  (0) 2022.07.27
오른손 법칙  (0) 2022.07.26

 

오른손 법칙

 

자료구조와 알고리즘 공부를 시작하겠다.

Visual Studio의 콘솔을 이용해서 간단한 맵을 만들어놓고 다양한 알고리즘을 실습해보겠다.

 


 

1-0. 실습을 진행할 map

콘솔 출력 창에 띄운 map이다. 

콘솔 관련 코드와 이중 반복문 등을 통해 제작한 것이고 자세한 건 생략하겠다.

 


 

우선 오른손 법칙이란 쉽게 설명하자면 미로에서 오른손을 벽에 대고 계속 이동한다고 생각하면 된다.

단순하고 무식한 방법이지만 웬만한 미로 등에 통하는 방법이기도 하다.

 

 

1-1. 플레이어 위치
1-2. 출구 위치

우선 1-1, 1-2 이미지와 같이 플레이어의 위치와 출구의 위치를 스택 메모리에 일시적으로 저장해놓는다.

저장해놓은 위치 정보를 토대로 플레이어가 출구에 도착할 때까지 반복하는 코드를 만들면 된다.

 


 

1-3. 오른쪽으로 이동
참고사항1
참고사항2

반복문을 이용해 플레이어가 출구에 도착할 때까지 반복하고 

오른쪽 길이 비어있는지 확인한 후 비어있다면 이동하는 코드이다.

 

_dir은 플레이어 클래스 내부에 선언된 플레이어가 바라보는 방향을 의미한다.

참고사항 이미지를 참고해서 코드를 해석하자면 

 

일단 오른쪽 방향을 보게 해야 하므로 _dir - 1을 해주고 음수가 있으면 안 되니까 DIR_COUNT를 더해준다.

그리고 DIR_COUNT를 나머지 연산을 해주게 되면 현재 바라보고 있던 방향에서 오른쪽으로 바라보게 된다.

이후에 오른쪽으로 이동할 수 있는지에 대한 함수를 만들고 현 위치에서 오른쪽으로 한 보 전진한 위치를 더하고 

해당 값을 매개값으로 넘겨준다. 그러면 만들어둔 CanGo 함수에서 해당 길이 비어있는지 확인해서 bool값을 뱉어준다.

바라보는 방향은 스택 메모리에 저장된 일시적인 값이기 때문에 실제 플레이어의 바라보는 방향을 바꾸기 위해

_dir += newDir을 실행해준다. 그리고 가야 하는 경로를 저장해주기 위한 vector 타입의 _path를 만들고

해당 벡터에 오른쪽으로 한 보 전진한  값을 넣어준다.

 

그외에 경우의 수도 다르지 않다.

오른쪽으로 갈 수 없다면 원래 바라보는 방향으로 일 보 전진하는 코드,

그것도 아니라면 왼쪽으로 방향을 회전하는 코드도 같은 원리를 이용하므로 생략하도록 하겠다.

 


 

이제 가야 할 경로를 벡터 _path가 저장하고 있는 상태이다. 

그럼 이제 _path를 이용해서 플레이어를 실질적으로 움직여야 한다.

움직이는 것은 Update에서 진행하면 된다.

 

 

1-4. 플레이어 움직이기
참고사항1
참고사항2

_pathIndex는 경로를 기준으로 어디까지 이동했는지 추적하기 위한 값이다. 

해당 값이 _path 사이즈보다 크면 더 이상 갈 수 있는 경로가 없다는 뜻이기 때문에 리턴을 한다.

그리고 플레이어가 움직이는 것이 너무 빠르기 때문에 이것을 조절하기 위한 변수 _sumTick를 만들고

현재 시간 경과를 뜻하는 deltaTick의 값을 더해준다. 해당 값이 100(ms)을 넘었을 경우 

다시 _sumTick의 값을 0으로 초기화 해주고 실제 위치를 저장해두었던 경로로 순서대로 바꿔준다.

 

 


 

1-5. 플레이어가 출구에 도착

간략하게 주요 코드만 알아보았고 여기까지 문제 없이 진행하면 1-5 이미지처럼 플레이어가

출구로 오른손 법칙을 이용해서 이동하는 것을 확인할 수 있다.

 

해당 map을 만드는 코드와 그외 자세한 코드들은 자료구조 알고리즘 강의를 다시 보자.

 

 

CPU, GPU 차이 간략하게 정리

 

1-0

CPU 

컴퓨터에서 발생하는 전반적인 연산을 담당하며 뛰어난 기억력을 가지고 있다. 

쉽게 말해 고급 인력이라고 생각하면 된다. ALU가 산술 연산을 담당하는 부분인데 1-0 이미지를 보면

CPU는 ALU가 상대적으로 많지 않다는 걸 알 수 있다. 

 

GPU

CPU에 비해 압도적으로 ALU가 많은 것을 확인할 수 있고 복잡하지 않은 연산을 빠르게 처리해준다.

좀 더 정확히 얘기하면 GPU는 연관성이 없는 독립적인 연산을 처리할 때 유용하다.

대표적으로 암호학이나 비트코인 채굴, 인공지능 같은 경우가 연관성이 없는 독립적인 연산이다.

 


게임 측면에서 생각해보자면 기본적으로 게임 안에 각각의 물체들은 서로에게 영향을 주지 않는다.

(물론 예외적인 상황이 많다.) 그렇기 때문에 물체의 좌표 같은 걸 계산하는 독립적인 연산을 GPU에게

넘기게 되고 GPU는 해당 연산을 받아서 병렬적으로 처리하게 된다.

 

 이러한 GPU는 3D 가상 세계에서 우리가 보는 모니터에 결과물을 출력해주기 위한 연산을 많이 하게 되며

해당 일련의 과정들을 '랜더링 파이프라인'이라고 한다.

 

랜더링 파이프라인에 관해서는 다음에 자세히 공부하겠다.

 

[Modern C++] 스마트 포인터 개론

 

이번에는 스마트 포인터에 대해 간략하게 알아보자.

 

우선 포인터에 대해 다시 알아보자면 포인터는 양날의 검과 같다.

장점은 메모리에 저장되어 있는 원본 데이터에 접근해서 수정 등이 가능하기 때문에 메모리 절약 및 효율성이

올라가지만, 단점은 메모리 오염이 일어나기 쉽다는 것이다. 

 

A와 B 클래스를 만들어서 동적 할당을 하고 A 클래스에서 B 클래스를 받아서 B 클래스의 데이터를 일부 수정한다고

가정하자. 그러던 도중 B 클래스가 어떤 사유에 의해 동적 할당이 해제되고 그 이후에 A 클래스가 B 클래스의 데이터를

수정하기라도 하면 바로 메모리 오염이 발생한다. 메모리 오염은 매우 치명적이기 때문에 절대로 발생해서는 안 되는 

실수이다.

 

이러한 단점을 보완한 것이 스마트 포인터이다. 좀 더 정확히 설명하자면 스마트 포인터는 

포인터를 알맞은 정책에 따라 관리하는 객체를 말한다. 

 

스마트 포인터는 크게 세 가지 개념이 존재한다.

shared_ptr, weak_ptr, unique_ptr

하나하나 알아보도록 하자.

 


 

shared_ptr

 

스마트 포인터에서 가장 대표 격인 친구이다.

shared_ptr은 레퍼런스 카운트를 관리한다. 말 그대로 참조 카운트, 즉 원본 포인터 객체를 몇 명이나 참조하고 있는지

카운트를 하고 있다가 아무도 해당 포인터 객체를 참조하고 있지 않을 때 비로소 delete를 진행한다.

 

또 한가지 스마트 포인터의 특징이 있는데, 사용자가 명시적으로 delete를 하지 않아도 자동으로 참조를 파악해서

delete를 진행하기 때문에 좀 더 수월한 코딩을 할 수 있다.

 

이제 shared_ptr의 단점을 얘기해 보자면, shared_ptr을 이용한 객체 A, B가 존재한다고 가정하자.

A는 B를 참조하고 B는 A를 참조하는 상황이다. 즉 서로가 서로를 참조하고 있다. shared_ptr의 특징은 참조하고 있는

객체가 있을 경우 메모리에서 할당이 해제되지 않는다는 점인데 이렇게 서로를 참조하고 있다면 두 객체는 메모리에서

절대 해제되지 않고 계속 남아있게 되는 문제점이 있다. 그렇기 때문에 메모리 할당을 해제하기 전에 포인터 객체를

받아주는 부분을 nullptr로 밀어주어서 할당을 해제해야 한다.

 


 

weak_ptr

 

weak_ptr의 경우 위에서 얘기한 shared_ptr의 단점을 보완하기 위한 ptr이다.

shared_ptr의 경우 내부적으로 참조 카운트를 관리하고 있는데 weak_ptr의 경우 참조 카운트와 더불어

weak 카운트를 관리한다. weak 카운트는 참조하고 있는 객체의 숫자를 나타낸다.

 

이러한 weak_ptr은 객체의 생명주기에 직접적으로 관여하는 친구는 아니지만 해당 객체가 메모리에서 

해제되었는지 아닌지를 체크해주는 기능을 한다.

참고로 expired()로 할당이 해제됐는지 아닌지 체크할 수 있다.

 

해당 객체가 메모리에 아직 존재한다면 lock() 기능을 이용해서 다시 shared_ptr로 변환을 해주고 원래

사용하던 것처럼 포인터를 이용하면 된다.

 

정리하자면 weak_ptr를 이용해서 포인터 객체를 받아주고 해당 포인터 객체가 아직 메모리에 존재한다면

shared_ptr로 변환해서 사용하는 방식이다. 즉 shared_ptr를 사용하기 전에 미리 체크한다는 개념으로 생각하면

될 것 같다. 

 


 

unique_ptr

 

unique_ptr의 경우 매우 간단하다.

말 그대로 나만 포인터 객체를 가지고 있는 가리키고 있는 형태라고 생각하면 된다.

기본적으로 다른 포인터에게 넘겨주지 못하지만 만약에 넘겨주려고 하면 오른값을 이용해서 넘겨줄 수 있다.

즉 복사는 허용해주지 않고 이동만 허용해주는 포인터라고 생각하면 된다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 람다 표현식  (0) 2022.07.19
[Modern C++] 전달 참조  (0) 2022.07.18
[Modern C++] 오른값(rvalue) 참조  (0) 2022.07.16
[Modern C++] override, final  (0) 2022.07.15
[Modern C++] nullptr  (0) 2022.07.15

 

[Modern C++] 람다 표현식

 

이번에는 람다를 알아보자.

람다란? 함수 객체를 빠르게 만드는 문법이다.

 

[ 이전에 공부했던 함수 객체를 다시 한번 간단하게 정리하자면 객체를 함수처럼 사용할 수 있는 문법이다.

즉 괄호()를 오버 로딩해서 객체가 함수처럼 동작하도록 만들어서 사용하는 것을 함수 객체라고 한다. ]

 

다시 돌아와서 람다는 이러한 함수 객체를 빠르게 만드는 문법이다. 잘 활용하면 10분 걸릴 코드를 1분 만에

작성할 수도 있다.

 


실습을 진행해보자

1-1. 실습 세팅

본격적으로 람다를 실습하기 전에 기본적인 세팅을 해주었다.

이전에 배운 enum class 문법을 이용해서 아이템 타입과 아이템의 레어도를 만들어주었고,

Item 클래스를 아이템 아이디, 레어도, 타입을 받아주는 형태로 만들었다.

 


 

1-2. 기본 함수 객체 사용

메인 함수에서 Item class를 가지는 vector를 선언해주고 push_back을 이용해서 Item class의 임시 객체를

넣어주는 모습이다. 임시 객체를 넣어주다 보니 이전에 공부했던 오른값 참조로 받아주는 모습이 보인다.

 


 

1-3. 기본 함수 객체 사용

어떤 아이템을 특정 조건에 맞춰서 찾고 싶은 상황이라고 가정하고 코드를 작성했다.

이전에 배웠던 벡터에서 조건에 맞는 데이터를 찾기 위해 find_if를 사용하고 벡터의 시작 지점부터 끝 지점까지

스캔하면서 조건에 맞는 데이터를 찾는 것이다. 마지막 조건 부분에 함수 객체를 만들어서 넣어주었고 해당 함수 객체는

오버 로딩을 이용해서 벡터에 저장되어 있는 아이템의 레어도가 Unique라면 true를 반환하도록 되어 있다.

결괏값으로 아이템 id가 정상 출력되는 것을 확인할 수 있다.

 

지금은 코드의 양이 적기 때문에 이미지 1-3과 같이 함수 객체를 struct나 class를 이용해서 만들고 사용해도 

큰 문제는 없지만 상황에 따라 코드의 양이나 아이템 등이 많아지면 위와 같이 매번 함수 객체를 만들어서 사용하기가

불편하다. 그래서 사용하는 것이 람다이다. 이제부터 람다를 실습해보자.

 


 

2-1. 람다 사용
2-2. 람다 타입 지정

람다는 기본적으로 '[]() {};' 이러한 형태로 사용한다. 이 형태는 이름이 정해지지 않은 익명 상태이다.

괄호 부분에 매개 값을 받아주고 중괄호 부분에 구현 부분을 넣어주면 된다. 따로 bool 타입으로 명시하지 않아도

자동으로 return 타입을 추측해서 넣어주게 되고 명시적으로도 타입을 지정할 수 있다. (이미지 2-2, int 타입 지정)

 

 

2-3. 람다 사용

만들었던 람다 표현식에 이름을 지정해주고 해당 이름을 기존의 함수 객체 자리에 넣어서 정상 작동

하는 것을 확인하였다. 그리고 62줄 코드 자체를 클로저(closure)라고 한다. 뜻은 람다에 의해 만들어진

실행 시점 객체라는 뜻을 가지고 있다.

 

 

람다 표현식에 이름을 지정해줘서 계속 사용할 수도 있지만 일회성으로 사용하고 싶다면 아래와 같이 할 수도 있다.

2-4. 람다 사용

find_if의 조건 부분에 람다 표현식을 넣어서 일회성으로 사용하는 방법이다.

이외에도 람다는 특정 데이터를 받아서 해당 데이터가 존재하는지 조건을 만들어주는 것도 가능하다.

 


 

3-1. 데이터를 받아주는 람다

 

[ ] 대괄호를 캡처라고 하며 변수 값을 복사하는 방식과 참조하는 방식이 존재한다.

3-1 이미지에서는 복사 방식을 이용했고 복사한 데이터(아이템 아이디)가 벡터 내에 존재하는지 확인한다.

 

사용자가 명시적으로 캡처를 지정하지 않으면 암시적으로 전부 복사 방식으로 동작하게 된다.

캡처를 참조 방식으로 하게 되면 변수의 주소 값을 받아주게 되는 차이만 생긴다.

 

 

3-2. 여러 데이터를 받아주는 람다

마찬가지로 하나의 데이터가 아닌 여러 데이터를 받아주는 람다를 만들었다.

 

 

3-3. 캡처 모드 각자 지정

이번에는 각 데이터마다 복사 방식으로 할 것인지 참조 방식으로 할 것인지 지정해주는 

방법이다. 대괄호 안에 해당 데이터마다 하나씩 지정해주면 된다.

 

일반적으로 한 번에 복사인지 참조인지 지정하는 방식보다는 데이터마다 복사, 참조를 지정해주는

방법을 좀 더 권장한다. 그 이유는 캡처 안에 데이터를 넣기 때문에 어떤 데이터가 있는지 알 수 있고

데이터마다 복사 방식인지 참조 방식인지 한눈에 알 수 있어서 가독성이 높아진다.

 

그리고 중요한 개념 하나는 특정 class 안에 함수를 만들고 그 안에 람다를 만들게 되면 캡처 안에는 

기본적으로 this 포인터가 존재하게 된다. 즉 해당 클래스의 주소 값을 가지고 있게 되므로 웬만하면

캡처 안에 복사(=)나 참조(&) 같이 한 번에 처리하는 방법 말고 데이터를 하나하나 지정해주는 방법이

권장된다. 

 

물론 람다를 외부로 넘길 일이 전혀 없다면 한 번에 처리해도 크게 상관은 없다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 스마트 포인터 개론  (0) 2022.07.19
[Modern C++] 전달 참조  (0) 2022.07.18
[Modern C++] 오른값(rvalue) 참조  (0) 2022.07.16
[Modern C++] override, final  (0) 2022.07.15
[Modern C++] nullptr  (0) 2022.07.15

 

[Modern C++] 전달 참조

 

오늘은 전달 참조에 대해 알아보자.

C++17 이전에는 전달 참조가 보편 참조로 불렸다.

보편 참조 == 전달 참조, 같은 의미이니 잘 숙지해두자.

 

 

일단 전달 참조 같은 경우 오른값 참조 문법과 거의 같다고 보면 된다.

1-1. 오른값 참조 복습겸 실습

Hoon 클래스를 생성하고 기본, 복사, 이동 생성자를 각각 만들어주었다.

그리고 별도의 함수를 생성해주고 오른값 참조를 받도록 선언하였다.

메인 함수에서 클래스 객체를 생성하고 별도로 만들었던 함수를 호출하며 전달해주는 값으로 클래스 객체를

넣어주는데 일반적으로는 넣을 수 없으니 std::move를 통해 오른값으로 캐스팅 후 넣어주었다.

 

하지만 오늘의 주제는 전달 참조이며 전달 참조는 오른값 참조와 눈으로 보기엔 다름이 없다.

즉 '&&' 처럼 코드가 만들어져 있어도 무조건 오른값 참조는 아니라는 소리이다. 

 

 

1-2. 전달 참조, 오른쪽 참조

28~32번 코드를 보면 템플릿을 통해 새로운 함수를 만들어주었고 전달받는 값으로는 '&&'로 선언해주었다.

그리고 메인 함수로 넘어가서 해당 함수를 호출해주었다.

39번 코드를 보면 37번 코드처럼 std::move를 통해 오른값 참조로 변환하여 넣어주어서 정상 작동한다.

근데 40번 코드를 보면 왼값 참조를 그냥 넣어주었는데 에러가 발생하지 않고 심지어 일반 참조 타입으로 

정상 호출되는 것을 알 수 있다.

 

해당 문제는 템플릿에만 해당되는 것이 아니라 auto에도 적용된다. 

정리하자면 컴파일러가 자동으로 타입을 추측해서 변환해주는 '형식 연역'이 일어날 때 전달 참조가 발생한다.

즉 29번 코드와 같이 템플릿 혹은 auto를 사용하는 것을 전달 참조라고 한다.

 

하지만 해당 코드를 변경하게 되면 전달 참조로서 동작하지 않는다. 29번 코드와 같이 'T&&'로만 만들어져

있어야 전달 참조이고 앞에 const 같은 것이 붙게 되면 오른값 참조로서 동작한다.

 

그렇다면 전달 참조는 왜 존재하는 것일까?

그냥 오른값 참조로서만 사용하면 안 되는 것일까?

 

전달 참조가 만들어진 이유는 효율성 때문이다.

왼값 참조와 오른값 참조를 동시에 가지고 있기 때문에 각각 만들어주지 않고 템플릿과 auto를

활용해 효율적으로 만들 수 있기 때문에 전달 참조가 사용된다.

(그렇다고 자주 사용되는 것은 아니다.)

 

 

다음은 자주 헷갈리는 부분을 알아보자

2-1. 헷갈리는 부분

참조 타입의 h2를 선언하고 미리 선언했었던 Hoon 클래스 타입의 h1을 받아주었다.

그리고 오른값 참조 타입으로 선언된 h3에 std::move를 이용하여 h2를 넣어주었다.

(참고로 Test1이라는 함수는 오른값을 받아주는 형태의 함수이다.)

오른값 참조 타입의 h3를 Test1 함수에 넣어주려고 하는데 이상하게 에러가 발생한다.

오른값을 받아주는 함수에 오른값 참조 타입의 변수를 넣었는데 왜 에러가 발생하는 것일까

 

그 이유는 h3가 오른값 참조 타입이지만 h3 자체는 왼값이기 때문이다.

오른값이라는 정의 자체가 단일식에서 벗어나면 사용할 수 없다고 했다. 즉 해당 라인을 벗어나면

다른 곳에서는 사용할 수가 없다. 하지만 h3의 경우 해당 라인을 벗어나서도 사용이 가능하다.

즉 타입만 오른값 참조 타입일 뿐 h3 자체는 왼값이다. 그래서 한 번 더 std::move를 이용해서 

함수에 넣어주어야 정상 작동한다.

 

 

진짜 문제는 지금부터이다.

위의 헷갈리는 문제에서 파생되는 문제인데 바로 전달 참조로 받아주었을 때 조건에 따라 

받아준 값을 왼값으로 처리해서 복사를 할지 아니면 오른값으로 처리해서 이동을 할지 선택해야 한다.

전달 참조 같은 경우 왼값과 오른값 모두 사용할 수 있기 때문에 이러한 문제가 생길 수 있다.

 

왼값인지 오른값인지 조건에 따라 나눠주는 기능을 따로 지원하는데 

std::forward가 해당 기능을 지원한다.

2-2. std::forward()

std::forward(value) 이렇게 사용하면 되고 value의 값이 왼값이라면 복사를 진행하고

value의 값이 오른값이라면 value는 오른값 참조를 가지고 있지만 value 자체가 왼값이기 때문에

한 번 더 std::move를 이용해서 오른값 참조로서 사용하게 된다.

 

즉 39번 코드가 실행되면 이동 생성자가 실행되고,

40번 코드가 실행되면 복사 생성자가 실행된다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 스마트 포인터 개론  (0) 2022.07.19
[Modern C++] 람다 표현식  (0) 2022.07.19
[Modern C++] 오른값(rvalue) 참조  (0) 2022.07.16
[Modern C++] override, final  (0) 2022.07.15
[Modern C++] nullptr  (0) 2022.07.15

+ Recent posts