분류 전체보기

반응형

2026년 입사를 앞두고 혼자 겨울 제주도 여행을 떠나기로 마음먹었다. 여행은 바로 다음 주, 급하게 계획을 세워본다!!

겨울은 아무래도 춥기 때문에 제주도에서 할 수 있는 게 딱히 없어서 뭘 할지 찾아보던 중에 눈 덮인 한라산이 눈에 보이는 것이 아닌가? 바로 한라산을 가야겠다는 계획을 세우고 관련 유튜브를 보았는데, 웬걸? 눈 덮인 아름다운 백록담을 보기 위해서는 따로 예약이 필요하다는 것이 아닌가?

아름다운 백록담

 

그래서 바로 예약 사이트를 들어가 봤지만 당연히 예약 완료.... 혹여나 취소표가 나올까 시간 날 때마다 들어가서 확인을 해 보았지만 당연히 쉽사리 취소표가 나오진 않았다....

 

그러던 와중에 "이렇게 인기가 많으면 누군가 매크로를 만들어 놓지 않았을까?..."라는 생각과 함께 구글에 한라산 매크로를 검색해 본다.

참고한 글

 

한라산에 가고 싶어

한라산으로 가는 길

velog.io

위 블로그에서 친절하게 매크로를 만들어 두시긴 했지만!! 나는 크롬 브라우저를 사용하지도 않고, 2026년 1월 기준 위 매크로는 이미 막힌(?) 느낌이기에 학부 컴공 출신인 내가!! 직접 수정하여 만들어 보도록 하였다. (물론 Gemini와 함께 ㅎ)

 

 

준비물

먼저 환경 세팅부터 말씀을 드리자면!

1. 파이썬이 설치되어 있을 것

 

Download Python

The official home of the Python Programming Language

www.python.org

위 사이트에서 그냥 인스톨하고 쭉쭉하면 된다.

 

2. selenium 패키지가 설치되어 있을 것

CMD를 열어서 아래 명령어를 입력하면 된다.

python -m pip install selenium webdriver-manager

 

3. 엣지 브라우저를 사용할 것

나는 홍대병이 있기 때문에 크롬을 쓰지 않는다. 우하하.

(실제로 메모리 소모량이 좀 더 낮다던데...)

 

4. 구글의 앱 비밀번호를 생성할 것

여석이 생길 경우에 매크로가 이메일을 로그인하여 메일을 보낼 수 있도록 앱 비밀번호를 따로 생성해야 한다.

해당 비밀번호는 앱만 사용하고 계정 주인이 원할 때 삭제할 수 있으니 걱정은 안 하셔도 된다는~

 

https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fmyaccount.google.com%2Fapppasswords%3Frapt%3DAEjHL4NztXAjaCS2RwEX-7NUOpxEmXMZu9d1JYZOMadRV_AZEoYEgTf6hsSwrzQtMeaimjemUDbhKFKWe0gCFR8jOiFIPRnYS4MYpZDxioCCwtOU8Vbx2wE&dsh=S621194386%3A1769269669975626&followup=https%3A%2F%2Fmyaccount.google.com%2Fapppasswords%3Frapt%3DAEjHL4NztXAjaCS2RwEX-7NUOpxEmXMZu9d1JYZOMadRV_AZEoYEgTf6hsSwrzQtMeaimjemUDbhKFKWe0gCFR8jOiFIPRnYS4MYpZDxioCCwtOU8Vbx2wE&ifkv=AXbMIuDGt1foeRDA6LQooi_VQC_o9ElKiZeoE41CRFm_A3eKhgXvpuQShB_xKZl67b_X_T5ITASv&osid=1&passive=1209600&rart=ANgoxceLudP-tUCqjZlSYZWGa5pFFE4j_GPRGeeNqBpvN2p3aWTf6WE9J_XlngEgja3WEWG9o0vjmRejXSntl01HD3SeV3O79KlKEGPhClsOSMTe9BjyIgc&service=accountsettings&flowName=WebLiteSignIn&flowEntry=ServiceLogin

 

accounts.google.com

위 링크에서 앱 이름을 만드시면 16자리 앱 비밀번호가 나오는데 해당 비밀 번호를 꼭 적어 두시기 바란다.

 

동작 방식

먼저 아래 한라산 예약 사이트를 들어가게 되면 아래와 같은 화면이다.

보시는 것과 같이 카카오 로그인으로 예약을 해야 된다.

로그인을 하게 되면

먼저 한라산 예약 사이트를 들어가게 되면 카카오 로그인으로 예약을 해야 한다. 로그인을 하게 되면 코스는 성판악 코스와 관음사 코스 두 코스만 예약이 필요하다.

 

매크로의 역할은 탐방로 선택, 탐방 날짜를 눌러서 현재 여석이 있는지를 판별하고, 여석이 있다면 메일을 통해서 여석이 있다고 알려 주는 것!

 

프로그램 실행 과정

1. 초기 정보 입력

먼저 프로그램을 시작하면 아래와 같이 알림을 보낼 Gmail 주소, 앱 비밀번호, 알림을 받을 이메일 주소(Gmail이랑 같아도 된다) 그리고 한라산 예약 페이지에 접속하기 위한 카카오 아이디 비번을 묻는다.

주의!! Gmail 아이디와 앱 비밀번호 및 카카오 아이디 비밀번호는 틀리면 판별하기 어렵기 때문에 꼭 정확하게 써야 정상 동작한다.

 

2. 탐방 정보 입력

그다음 가고자 하는 날짜, 탐방 코스 그리고 시간대를 묻는다. 만약 여러 날이 예비 후보에 있다면 ',' 로 구분해서 날짜를 작성해 주면 된다. 주의할 점은 현재 사용하는 달과 같은 달의 날짜만 사용이 가능하다.

 

3. 탐색 결과

그럴 경우 매크로가 현재 예약 현황을 실시간으로 돌리게 된다.

 

여러 날이 예비 후보에 있는 경우 아래와 같이 순차적으로 서칭을 진행한다.

28일과 20일 매크로

 

4. 이메일 발송

만약 여석이 생기게 된다면 이메일 함에 알림 메일이 온다!

 

알림이 오면 초스피드로 들어가서 예약을 하면 된다.

맨 아래는 2 여석인데, 거진 몇 초 만에 한 명 채워짐... 

 

나도 위 메일을 보고 실제로 예약 성공했다!!

 

코드 공개

그래서 매크로 프로그램을 내놓으라고요?

혹시 사용하는 사람들 중에 "아니 Gmail 아이디 비번이랑... 카카오 아이디 비번을 블로그 주인이 빼가는 거 아니야??"라는 생각이 들 수 있다.

그렇기 때문에 코드를 모두 공개한다.

import time
import smtplib
import traceback
import getpass
from email.mime.text import MIMEText
from selenium import webdriver
from selenium.webdriver.edge.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select, WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import StaleElementReferenceException, UnexpectedAlertPresentException

print("="*60)
print("   한라산 탐방 예약 매크로 (pretending.tistory.com)")
print("="*60)
GMAIL_USER = input("1. 알림 보낼 Gmail 주소: ")
GMAIL_APP_PW = getpass.getpass("2. Gmail 앱 비밀번호(16자리): ")
RECIPIENT_EMAIL = input("3. 알림 받을 이메일 주소: ")
KAKAO_ID = input("4. 카카오 ID (이메일): ")
KAKAO_PW = getpass.getpass("5. 카카오 비밀번호: ")

print("="*60)
# 1. 날짜 입력
input_dates = input("1. 확인하고 싶은 날짜 입력 (예: 28,29): ")
date_list = [d.strip() for d in input_dates.split(',')]

# 2. 코스 선택
print("\n2. 탐방 코스를 선택하세요")
print("1) 성판악  2) 관음사  3) 둘 다 상관없음")
course_choice = input("선택 (번호 입력): ")

# 3. 시간대 선택
print("\n3. 탐방 시간대를 선택하세요")
print("1) 05:00~08:00 (새벽)  2) 08:01~11:30 (오전)  3) 둘 다 상관없음")
time_choice = input("선택 (번호 입력): ")

# 4. 기타 설정
CHECK_INTERVAL = int(input("\n4. 체크 간격(초, 기본 3): ") or "3")
headless_input = input("5. 백그라운드 실행 여부 (Y/N, 기본 Y): ").upper() or "Y"
HEADLESS_MODE = True if headless_input == 'Y' else False

# --- 데이터 매핑 테이블 ---
COURSES = {
    "1": [("성판악", "242")],
    "2": [("관음사", "244")],
    "3": [("성판악", "242"), ("관음사", "244")]
}
TIMES = {
    "1": [("05:00~08:00", "TIME1")],
    "2": [("08:01~11:30", "TIME8")],
    "3": [("05:00~08:00", "TIME1"), ("08:01~11:30", "TIME8")]
}
CAPACITY_MAP = {
    "242_TIME1": 800, "242_TIME8": 200,  # 성판악
    "244_TIME1": 400, "244_TIME8": 100   # 관음사
}

# 탐색 타겟 리스트 자동 생성
selected_courses = COURSES.get(course_choice, COURSES["3"])
selected_times = TIMES.get(time_choice, TIMES["3"])

targets = []
for d in date_list:
    for c_name, c_val in selected_courses:
        for t_text, t_val in selected_times:
            cap_num = CAPACITY_MAP.get(f"{c_val}_{t_val}", 0)
            targets.append((d, c_name, c_val, t_text, t_val, f"{cap_num} / 정원 {cap_num}"))

print("="*60)
print(f"총 {len(targets)}개의 조합으로 모니터링을 시작합니다.")

# --- [3. 기능 함수 구간] ---

def send_email(subject, body):
    try:
        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.starttls()
        server.login(GMAIL_USER, GMAIL_APP_PW)
        msg = MIMEText(body)
        msg['Subject'] = subject
        msg['From'] = GMAIL_USER
        msg['To'] = RECIPIENT_EMAIL
        server.sendmail(GMAIL_USER, RECIPIENT_EMAIL, msg.as_string())
        server.quit()
        print(f"이메일 발송 성공!")
    except Exception as e:
        print(f"이메일 발송 실패: {e}")

def get_driver():
    options = Options()
    options.add_argument("--disable-blink-features=AutomationControlled")
    options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"])
    
    if HEADLESS_MODE:
        options.add_argument("--headless=new")
        options.add_argument("--disable-gpu")
        options.add_argument("--window-size=1920,1080")
        print("Headless 모드로 실행 중...")
    
    driver = webdriver.Edge(options=options)
    if not HEADLESS_MODE:
        driver.maximize_window()
    return driver

def login(driver):
    print("로그인을 시도합니다...")
    driver.get('https://visithalla.jeju.go.kr/login/login.do')
    wait = WebDriverWait(driver, 20)
    
    login_btn = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'btn-kakao')))
    login_btn.click()
    time.sleep(3) 

    main_window = driver.current_window_handle
    wait.until(lambda d: len(d.window_handles) > 1)
    for handle in driver.window_handles:
        if handle != main_window:
            driver.switch_to.window(handle)
            break

    wait.until(EC.presence_of_element_located((By.NAME, 'loginId'))).send_keys(KAKAO_ID)
    driver.find_element(By.NAME, 'password').send_keys(KAKAO_PW)
    driver.find_element(By.CLASS_NAME, 'btn_g').click()
    
    time.sleep(4) 
    driver.switch_to.window(main_window)
    wait.until(EC.url_contains("main.do"))

def go_reservation_page(driver):
    wait = WebDriverWait(driver, 15)
    actions = ActionChains(driver)
    driver.get("https://visithalla.jeju.go.kr/main/main.do")
    time.sleep(2)
   
    reserve_menu = wait.until(EC.presence_of_element_located((By.LINK_TEXT, "탐방로예약")))
    actions.move_to_element(reserve_menu).perform()
    time.sleep(1)
    wait.until(EC.element_to_be_clickable((By.LINK_TEXT, "선착순예약"))).click()

def check_target(driver, date, course_name, course_val, time_text, time_val, capacity_text):
    wait = WebDriverWait(driver, 15)
    try:
        try:
            alert = driver.switch_to.alert
            alert.accept()
        except:
            pass

        # 코스 선택
        course_el = wait.until(EC.presence_of_element_located((By.ID, "courseSeq")))
        Select(course_el).select_by_value(course_val)
        time.sleep(0.5)

        # 날짜 선택
        wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'ui-datepicker-trigger'))).click()
        time.sleep(0.5)
        wait.until(EC.element_to_be_clickable((By.XPATH, f"//a[text()='{date}']"))).click()
        time.sleep(1)

        # 시간 선택
        time_el = wait.until(EC.presence_of_element_located((By.ID, "visitTm")))
        Select(time_el).select_by_value(time_val)
        time.sleep(0.5)

        # 인원 확인
        current_num_el = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '#current_num > strong')))
        status_text = current_num_el.text.strip()
        print(f"{date}일 {course_name} [{time_text}] : {status_text}")

        if capacity_text not in status_text:
            print(f"취소표 발생!! [{date}일 {course_name}]")
            send_email(f"[한라산] {date}일 {course_name} 취소표!", 
                       f"일시: {date}일\n코스: {course_name}\n시간: {time_text}\n상태: {status_text}")
            return True
            
    except Exception as e:
        print(f"{date}일 {course_name} 확인 실패 (스킵)")
    return False

def main():
    driver = None
    try:
        driver = get_driver()
        login(driver)
        
        print("\n매크로를 시작합니다. 종료하려면 Ctrl+C를 누르세요.\n")
        
        go_reservation_page(driver)
        time.sleep(1)
        while True:
            print(f"\n사이클 시작: {time.strftime('%H:%M:%S')}")
            for t in targets:
                check_target(driver, *t)
                time.sleep(1)
            
            print(f"한 사이클 완료. {CHECK_INTERVAL}초 후 재시작.")
            time.sleep(CHECK_INTERVAL)
    
    except KeyboardInterrupt:
        print("\n\n사용자가 프로그램을 중단했습니다.")
    except Exception as e:
        print(f"\n치명적 오류: {e}")
        traceback.print_exc()
    finally:
        if driver:
            driver.quit()
            print("프로그램 종료 완료")

if __name__ == "__main__":
    main()

 

더불어 "아니 블로그 주인 믿는데 코드 가지고 뭘 하라고.."라는 분들을 위해서 파이썬 파일도 올려 두겠다. (그냥 위 코드를 .py에 복붙 한 파일이다)

한라산_매크로.py
0.01MB

 

매크로 프로그램은 한라산 예약 페이지가 수정됨에 따라서 추후 동작을 안 할 수 있다.
최대한 유지보수를 할 예정이니 많은 관심 부탁드린다.

 

 

 

본 프로그램 사용으로 인해 발생하는 모든 결과에 대한 책임은 사용자 본인에게 있으며, 제작자는 어떠한 결과에 대해서도 책임을 지지 않습니다.

 

반응형
반응형

  

반도체 설계의 게임 체인저: RISC-V(리스크 파이브) 핵심 가이드

최근 삼성전자, SK하이닉스, 엔비디아(NVIDIA) 등 글로벌 반도체 거인들이 공통적으로 언급하는 키워드가 있습니다. 바로 RISC-V입니다. 주식 투자자에게는 'ARM의 대항마'로, 설계 엔지니어에게는 '설계의 자유를 선사하는 새로운 표준'으로 다가오는 RISC-V의 실무 포인트를 짚어보겠습니다.

RISC-V 프로세서 프로토타입

 

1. RISC-V, 왜 알아야 할까요?

CPU를 설계할 때 하드웨어가 소프트웨어의 명령을 알아듣기 위한 약속을 명령어 집합 구조(ISA, Instruction Set Architecture)라고 합니다.

  • 비용 절감: 영국의 ARM이나 미국의 Intel과 달리 오픈소스입니다. 막대한 로열티를 지불하지 않아도 되므로 비용 효율적입니다.
  • 반도체 자립: 특정 국가나 기업의 라이선스 정책에 휘둘리지 않고 독자적인 칩 생태계를 구축할 수 있습니다.

 

2. 핵심 개념: 필요한 기능만 골라 쓰는 '레고' 구조

RISC-V는 모든 기능을 다 담고 있는 무거운 종합 세트가 아닙니다. 기본 뼈대(Base)에 필요한 기능만 확장팩(Extension)으로 붙이는 모듈러(Modular) 방식이 가장 큰 장점입니다.

확장팩 명칭 주요 기능 실무적 의도 (PPA 관점)
I Integer 기본 정수 연산 (필수) 최소 로직 게이트로 초저전력 구현
M Multiply 고속 곱셈/나눗셈 추가 산술 연산 성능 및 Cycle 효율 증대
A Atomic 원자적 메모리 연산 멀티코어 환경의 데이터 정합성 보장
F / D Floating 부동소수점 (Single/Double) 고정밀 그래픽 및 수치 해석 지원
V Vector 병렬 연산 (SIMD) AI 가속 및 대용량 데이터 병렬 처리
C Compressed 16비트 압축 명령어 코드 밀도 향상 및 메모리 점유율 절감
B Bit Manipulation 비트 단위 조작 연산 통신 프로토콜 및 암호화 연산 최적화
K Scalar Crypto 스칼라 암호화 확장 보안 칩 및 임베디드 보안 강화

 

3. RISC-V는 프로세서(제품)가 아닙니다

가장 많이 오해하는 부분입니다. RISC-V는 Intel의 i7이나 ARM의 Cortex 같은 하드웨어 제품명이 아니라 '언어 규격(ISA)'입니다.

  • 규격(Spec): "ADD 명령어는 이렇게 생겨야 한다"는 약속.
  • 구현(Implementation): 파이프라인을 몇 단으로 할지, 캐시 크기를 얼마로 할지 등은 설계자의 몫입니다.

규격만 맞춘다면 그 안을 어떻게 요리하든 설계자의 자유입니다. 삼성전자가 RISC-V 코어를 설계한다는 것은 ARM의 설계도를 가져오는 게 아니라, 규격에 맞춰 직접 RTL을 코딩하여 독자적인 하드웨어를 만든다는 의미입니다.

 

4. RTL / Hardware 설계 관점 포인트

RTL 설계자에게 RISC-V는 내부 로직을 직접 수정하고 최적화할 수 있는 강력한 도구입니다.

① 단순한 파이프라인 구조

RISC-V는 Load-Store 아키텍처를 사용하여 메모리 접근과 연산 동작을 엄격히 분리합니다. 덕분에 하드웨어 설계 시 파이프라인 단계를 단순하고 명확하게 유지할 수 있어 버그 발생 확률이 줄어듭니다.

② Verilog 예시: 명령어 디코딩(Decoding)

// RISC-V 명령어 해석의 기본 원리 (예시)
always_comb begin
    case(opcode)
        7'b0110011: begin // R-type (예: ADD, SUB)
            reg_write = 1'b1;
            alu_src   = 1'b0; // 레지스터에서 값을 가져옴
        end
        7'b0000011: begin // I-type (예: LW - 메모리 읽기)
            reg_write = 1'b1;
            alu_src   = 1'b1; // 즉치값(Immediate)을 사용
        end
        default: begin
            reg_write = 1'b0;
            alu_src   = 1'b0;
        end
    endcase
end

 

5. 실무 관점 포인트: PPA와 검증의 트레이드오프

  1. PPA 최적화 (Custom Instruction):
    표준 명령어 외에 나만의 명령어를 추가할 수 있습니다. 예를 들어 AI 연산에 특화된 로직을 명령어 하나로 실행하게 만들면, 범용 CPU 대비 전력과 면적(Area)에서 엄청난 이득을 봅니다.
  2. 검증(Verification)의 책임:
    ARM 같은 기성품은 검증이 끝난 코드를 사오는 것이지만, RISC-V는 직접 수정하고 확장한 만큼 검증도 설계자의 몫입니다. 따라서 SystemVerilog나 UVM 같은 고급 검증 기술이 더욱 중요해집니다.

 

3줄 요약

  1. RISC-V는 로열티가 없는 오픈소스 반도체 설계 규격(ISA)이다.
  2. 모듈형 구조와 커스텀 명령어 추가 기능을 통해 AI, IoT 등 특수 목적 칩 설계에 최적이다.
  3. 설계 자유도가 높은 만큼, 설계자의 RTL 구현 능력과 검증 역량이 곧 경쟁력이 된다.
반응형
반응형

STL (Standard Template Library)의 첫 번째 Sequence Container (연속적인 컨테이너)인 std::vector에 대해서 공부를 한다. 

 

 

std::vector

 

Vector는 Dynamic Size Array (동적 배열)의 Sequence Constainer (연속적인 컨테이너)이다. 그 뜻은 우리가 일반적으로 특정 Type에 대해서 포인터를 만들고 동적으로 메모리를 할당하는 것과 동일하게 Heap영역에 동적으로 Type에 대한 메모리를 Dynamic 하게 생성해준다. 

 

아래는 일반적으로 포인터를 이용해서 Dynamic Array를 생성한 코드이다. 

 

int main()
{
    int* arrayPtr = new int[10];
    
    for(int i = 0; i < 10; ++i)
    {
        arrayPtr[i] = i;
    }
    
    delete[] arrayPtr;
    
    return 0;
}

 

위 코드와 같이 new키워드를 통해서 integer type의 크기 10자리 메모리 공간을 동적으로 할당하여 그 공간을 arrayPtr이 가리키는 코드이다. 위 코드의 단점은 꼭 해당 메모리 공간을 개발자가 delete를 시켜줘야 한다. 하지만 std::vector는 이러한 공간을 관리를 해준다. 

 

std::vector의 선언은 아래 코드와 같다. 

 

#include <vector>

int main()
{
    std::vector<int> v(10);
    for(int i = 0; i < 10; ++i)
    {
        v[i] = i;
    }
    
    return 0;
}

 

std::vector를 이용해서 포인터로 동적 할당한 코드와 동일한 코드를 짜보았다. 먼저 std:vector를 사용하기 위해서는 vector헤더를 포함시켜주어야 한다. 그리고 Angle Bracket ('<', '>') 안에 생성하고자 하는 메모리의 타입을 지정을 해주어야 한다 그리고 괄호 안에 크기를 입력해주면 된다. 위 코드에서는 크기를 주었지만 크기를 따로 주지 않고 메모리를 초기화하는 방법이 존재한다. 

 

#include <vector>

int main()
{
    std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    
    return 0;
}

 

선언과 동시에 원소를 초기화 하는 방법은 위와 같다. 

 

이제 생성된 std::vector의 원소 값을 출력을 해보자 std::vector의 원소 출력은 다양한 방식으로 할 수 있는데 크게 3가지를 소개한다. 

 

#include <vector>
#include <iostream>

int main()
{
	std::vector<int> nums(10);

	//[] indexing
	for (std::size_t idx = 0; idx < nums.size(); ++idx)
	{
		std::cout << nums[idx] << std::endl;
	}

	//iterator
	for (auto itr = nums.begin(); itr != nums.end(); ++itr)
	{
		std::cout << (*itr) << std::endl;
	}

	//ranged for (most optimized)
	for (const int& num : nums)
	{
		std::cout << num << std::endl;
	}
}

 

위 세개의 방법은 모두 동일한 출력을 만들어 낸다. 

 

먼더 [] iundecing은 우리가 평소에 배열의 모든 원소를 출력하고자 할 때 자주 쓰는 방법이다. 두 번째는 vector의 iterator를 사용하는 방법이다. itr의 형은 auto로 자동적으로 위 코드에서는 vector <int>::iterator가 된다. 초기값은 nums의 begin() 메서드를 통해서 nums의 시작점을 가리키고 nums가 마지막일 때까지 출력을 하게 된다. 마지막은 ranged for loop (범위 기반 루프)이다. 쉽게 생각해서 num이라는 레퍼런스에 nums 주고 값을 저장하고 배열 출력에 흑화 된 for loop이라고 생각하면 된다. 레퍼런스가 없어도 무방하지만 레퍼런스가 없다면 nums에 대한 복사가 일어나기 때문에 레퍼런스를 붙여주되 배열의 원소의 조작이 없다면 const를 붙여주는 것이 이상적이다. 또한 auto를 이용하여 Type의 추론을 넣어 작성하는 것이 이상적이다. 해당 방법이 가장 Optimized 된 방법이라 해당 방법을 추천한다. 

 

위 코드에서 사용된 begin(), size(), end()메소드는 vector의 메소드이다. 이외에도 수 많은 메소드들이 있기 때문에 다 설명을 하지않고 cppreference를 참고하기 바란다. 

 

 

Ref.

https://nocodeprogram.com/lecture/1/83003/

https://en.cppreference.com/w/cpp/container/vector

반응형

[C++] C++20 찍먹 Concepts

2021. 8. 28. 00:12
반응형

C++20부터 적용된 기능인 Concept에 대해서 아주아주 간단하게 알아보자. Concept는 Template을 이용할 때 Compile-time에 Template에서 의도치 않는 연산을 방지해주는 기능이다. 아직은 많이 사용되는 기능이 아니라 많이 소극적으로 사용을 하되 표준화과 되면 적극적인 사용을 하라고 권하고 싶다. 

 

 

Concepts

 

Concept은 Template에 의도한 연산을 방지해주는 기능이다. 예를 들면 아래와 같은 코드가 있다고 하자. 

 

template <typename T>
T mulTwo(T a, T b)
{
    return a + b;
}

 

위와 같은 template함수가 있다면 Template함수를 만든 개발자는 +연산이 가능한 integer나 double의 연산을 하기 위해 만들었다. 하지만 다른 사람이 이 함수를 보고 std::string객체를 넘겨서 두 문자열이 합쳐지는 기능으로 사용됐다고 생각하면 개발자의 의도가 아닌 다른 방향으로 사용이 되게 된다. 아무리 주석을 이용해서 사용했으면 하는 Data Type을 써놓아도 방지가 어렵기 때문에 뒤에 require 키워드를 통해서 사용 가능한 Type을 적어주고 Compile-time에 검사를 하는 방법으로 해당 문제를 방지할 수 있다. 

 

아래 코드는 concept의 사용법과 Template Function에 require을 통해서 사용 가능한 type을 명시해주는 예이다.

#include <iostream>
#include <concept>

template<typename T>
concept type_that_can_be_used = std::integral<T> || std::floating_point<T>;		//integer Type, float Type만 사용이 가능하게 만든 concept


template<typename T>
T mulTwo(T a, T b)
{
	return a + b;
}


int main()
{
	std::cout << mulTwo(1, 2) << '\n';
	std::cout << mulTwo(1.2f, 1.5f) << '\n';
	std::cout << mulTwo<std::string>("Hi~", " pretending");	

}

 

위 코드는 concept를 만들어 주었지만 Template Function에서 requires키워드를 통해 concept를 불러오지 않았다. 그렇기 때문에 위 코드는 컴파일이 정상적으로 진행된다. 

 

#include <iostream>
#include <concept>

template<typename T>
concept type_that_can_be_used = std::integral<T> || std::floating_point<T>;		//integer Type, float Type만 사용이 가능하게 만든 concept


template<typename T>
T mulTwo(T a, T b) requires type_that_can_be_used<T>
{
	return a + b;
}


int main()
{
	std::cout << mulTwo(1, 2) << '\n';
	std::cout << mulTwo(1.2f, 1.5f) << '\n';
	//std::cout << mulTwo<std::string>("Hi~", " pretending");	 ERROR

}

 

위와 같이 requires를 통해 만들어준 concept을 적어주면 Compile-time에 std::string은 ERROR가 나게 해 준다. concet을 선언할 때는 concpet이름과 뒤에 concept 라이브러리에 있는 Sepcifier를 이용해야 하는데 엄청 많다. 

더 많으니 cppreference를 참고하기 바란다. 

 

 

concept 간단하게 알아보고 자세히 알아보고 싶다면 아래 cppreference를 참고하기 바란다. 

 

 

Ref.

https://en.cppreference.com/w/cpp/concepts

https://www.youtube.com/watch?v=phZbmGNGqew&list=PLDV-cCQnUlIb2oezNpNTmxiiX_NibMrlO&index=6&t=329s 

 

반응형
반응형

이번 포스트에서 다룰 내용은 다양한 Template에 대해서 공부한다. 이제까지 배운 Template은 Temlate Function밖에 없었지만 이번에는 다양한 Template에 대한 사용법을 알아본다. 먼저 배울 Template은 아래와 같다. 

 

  • Class Template
  • Aliasing Temlate
  • Variable Template

 

 

Class Template (클래스 템플릿)

 

Class template은 우리가 알게 모르게 사용을 많이 한다. STL(Dtandard Template Library) 자체가 Template으로 구성된 라이브러리이다. Class Template도 Template Function과 동일하게 사용을 하면 된다. 필자는 Template에 대해서 공포감을 느끼기 때문에 충분히 이해가 가능하지만 거부감이 든다. 아래는 Template을 이용해서 push와 pop만 가능한 Data Sturcture의 Stack을 만든 예제이다. 

 

이미 다 공부한 내용이라 코드 첨부로 설명을 마친다. 

 

template<typename T>
class Stack
{
public:
	void push(T elem)
	{
		mVec.emplace_back(std::move(elem));
	}
	bool pop(T& elem)
	{
		if (mVec.size() == 0)
			return false;
		elem = mVec[mVec.size() - 1];
		mVec.pop_back();
		return true;
	}
private:
	std::vector<T> mVec;
};

int main()
{
	Stack<std::string> stack;

	stack.push("jiyong");
	stack.push("is");
	stack.push("he");

	std::string n;
	while (stack.pop(n))
	{
		std::cout << n << '\n';
	}
}

 

 

Aliasing Template (별명 템플릿?)

 

Alias는 별명 혹은 별칭이라는 뜻을 가지고 있다. 결국 Aliasing Template은 별명을 가지게 하는 게 Template을 이용해서 별명을 만든다고 생각하면 쉽다. 

 

#include <vector>
#include <array>

template<typename T>
using pretendKeys = std::vector<std::array<T, 64>>;

int main()
{
	/*
	* alias
	* using pretendInt = int;
	* using pretendKeys = std::vector<int*_t, 64>;
	*/
	
	pretendKeys<float> floatKeys;
	//std::vector<std::array<float, 64>> floatKeys
	pretendKeys<double> doubleKeys;
	//std::vector<std::array<double, 64>> doubleKeys
}

 

alias을 만드는 법은 using 키워드를 통해 만들 수 있다. C++03에서는 typedef를 사용했지만 C++11에서부터 using을 사용해서 더 직관적이게 "별명"을 붙일 수 있다. 

 

 

Variable Template (변수 템플릿) C++14

 

먼저 Variable Template을 설명하기 전에 const와 constexpr에 대해서 짧게 설명한다. 

 

int main()
{
    int n = 10;
    const int a = 10 + n;
    
    constexpr int b = 11 + n;	//ERROR 컴파일 타임에 n이 정해지지 않음.

    return 0;
}

 

const와 constexpr 둘 다 상수를 선언하는 키워드이다. 둘의 차이점은 컴파일 상수이냐 런타임 상수이냐의 차이이다. const는 런타임에 초기값이 정해져 있어야 하고 constexpr는 컴파일 타임에 초기값이 정해져 있어야 한다는 차이점이다. 

 

이제 Variable Template (변수 템플릿)에 대해서 공부를 하자. 

 

변수 템플릿도 별거 없다. C++14부터 지원되는 기능이다. 아래 예제를 보고 충분히 이해할 수 있다. 

 

#include <iostream>

template<typename T>
constexpr T number = T(123.123);

int main()
{
    std::cout << number<int>;

    return 0;
}

 

Variable Template으로 number라는 상수를 선언하고 123.123이라는 값으로 초기화를 하였다. 

 

main함수에서 number상수에 Anlge Bracket으로 Type을 int로 명시하여 출력해주면 integer값으로 Casting이 되고 123이 출력이 된다. 

 

 

Ref.

https://www.youtube.com/watch?v=87nJ-3U4LkA&list=PLDV-cCQnUlIb2oezNpNTmxiiX_NibMrlO&index=5 

반응형
반응형

Template Build (Instantiation)은 C++ 코드를 짤 때 Header와 Cpp파일을 분리해서 코드를 따게 되는데 Template 함수를 Header에 Declaration을 하고 Cpp파일에 Definition을 하고 컴파일을 하면 에러가 뜨고 컴파일이 되지 않는다. 해당 문제가 왜 발생하는지 알아보자. 

 

 

Template Instantiation (템플릿 인스턴스화)

 

템플릿 인스턴스화에는 크게 아래 두 가지 종류가 있다. 

  • Implicit Instantiation (암시적 인스턴스화)
  • Explicit Instantiation (명시적 인스턴스화)

 

 

Implicit Instantiation (암시적 인스턴스화)는 우리가 Template함수를 정의하고 main함수에서 Angle Bracket으로 차입을 명시해주어 함수를 호출하는 방식을 말한다. 암시적 인스턴스화는 두 가지가 있는데 다 이미 배웠던 내용이다. 

 

코드를 통해서 두 가지 암시적 인스턴스화에 대해서 알아본다. 

 

암시적 인스턴스화는 두 가지가 있다. 

  • 사용자가 Angle Bracket ('<', '>')에 Type을 명시해주는 방법
  • Template Type Deduction (타입 추론)을 이용해 Type을 명시하지 않는 방법
#include <iostream>

template<typename T>
T foo(T a)
{
	std:::cout << a << '\n';
}
int main()
{
	foo<int>(12);	//사용자가 Type을 직점 Angle Bracket에 넣어주는 방법

	foo(12);	//Template Type Deduction을 이용해 Type을 명시해주지 않는 방법

	return 0;
}

 

Template 함수는 함수를 호출(인스턴스화) 하기 전까지 코드로 존재하다가 인스턴스 화가 되면서 해당 Type혹은 Deduction을 통해서 함수를 만들게 된다. 

 

그렇다면 Header 파일에 Template함수에 대한 Declaration을 작성하고 Cpp 파일에  Definition을 작성하게 되면 정상적으로 코드가 동작하는지 확인해보자. 

 

main.cpp

#include <iostream>

int main()
{
	foo<int>(12);	//사용자가 Type을 직점 Angle Bracket에 넣어주는 방법

	foo(12);	//Template Type Deduction을 이용해 Type을 명시해주지 않는 방법

	return 0;
}

 

foo.h

#pragma once
template<typename T>
T foo(T a);

 

foo.cpp

template<typename T>
T foo(T a)
{
	std:::cout << a << '\n';
}

 

위와 같이 Template 함수의 Declaration를 foo.h에 함수의 Definition을 foo.cpp에 적는다면 컴파일 에러가 나게 된다. 컴파일 에러가 나게 되는 이유는 Template는 함수 자체가 아니고 컴파일러가 Instantiation이 발생하면 코드로 만들어주는 기능을 하게 된다. 여기서 Template을 보고 코드를 만들게 되는데 foo.h에 있는 내용 즉 Declaration만 보고는 어떠한 코드를 만들지를 모르기 때문에 컴파일 에러가 난다. 

 

위 에러를 해결하기 위해서 두 가지 방법을 사용할 수 있는데 코드로 알아본다. 

 

main.cpp

#include <iostream>

int main()
{
	foo<int>(12);	//사용자가 Type을 직점 Angle Bracket에 넣어주는 방법

	foo(12);	//Template Type Deduction을 이용해 Type을 명시해주지 않는 방법

	return 0;
}

 

foo.h

#pragma once
template<typename T>
T foo(T a)
{
	std:::cout << a << '\n';
}

 

위와 같이 cpp파일에 Definition을 따로 적지 않고 header 파일에 정의까지 해주는 방법이 있다. 

 

굳이 cpp 파일에 정의를 해서 코드를 만들고 싶다면 cpp 파일 안에 Type Explicit Instantiation을 하면 문제가 해결된다. 이 뜻은 무엇이냐면 cpp 파일 안에 특정 Type에 대한 함수로 컴파일을 해달라고 요청한다고 생각하면 된다. 하지만 이럴 경우 사용하고자 하는 Type에 대해서 모두 Explicit 하게 선언을 해주어야 한다. 

 

main.cpp

#include <iostream>

int main()
{
	foo<int>(12);	//사용자가 Type을 직점 Angle Bracket에 넣어주는 방법

	foo(12);	//Template Type Deduction을 이용해 Type을 명시해주지 않는 방법

	return 0;
}

 

foo.h

#pragma once
template<typename T>
T foo(T a);

 

foo.cpp

template<typename T>
T foo(T a)
{
	std:::cout << a << '\n';
}

template int foo<int>(int);	//explicit template instatiation

 

반응형
반응형

Template으로 만들어진 함수를 사용하기 위해서는 함수명 뒤에 Argument Type을 명시를 해주어야 했다. 하지만 Argument Type을 명시하지 않아도 컴파일러가 Argument를 보로 Deduction (추론)을 하여 해당 타입의 함수를 만들어 준다. 

 

 

Template Type Deduction (템플릿 타입 추론)

 

Template으로 구선된 함수는 타입을 추론할 수 있다. 아래 코드는 타입이 명시됐을 때와 안 됐을 때의 예를 보여주는 코드이다. 

 

#include <iostream>

template<typename T>
void printVar(T a)
{
    std::cout << typeid(a).name() << std::endl;
    std::cout << a << std::endl;
}


int main()
{
    int a = 100;
    
    printVar<int>(a);	//type 명시
    
    printVar(a);	//type 미명시
    
    return 0;
}

 

main함수에서 Type을 명시하여 Template함수를 호출하는 것과 Type을 명시하지 않고 Template함수를 호출하였다. 출력 화면은 아래와 같다. 

 

출력 콘솔에서 확인을 하게 되면 두 호출 모두 int 타입의 함수가 만들어지고 Argument로 넘어간 값도 출력이 잘 되었다. 이와 같이 Template 함수는 Argument를 보고 Type을 Deduction (추론)을 하게 된다. 일만 integer나 string클래스에 대해서는 다 문제없이 잘 작동하는데 Parameter를 Reference로 받거나 R-Value로 받을 경우는 어떻게 되나 궁금할 수 있다. 

 

Template 함수도 일반 함수와 같이 Reference Type 혹은 R-value Reference Type으로 받는 것이 가능하다. 먼저 Reference와 R-Value Reference의 일반 함수를 만들어 보면 아래와 같다. 

 

#include <iostream>

template<typename T>

void printLRef(int& a)
{
    std::cout << a << std::endl;
}

void printRRef(int&& a)
{
    std::cout << a << std::endl;
}

int main()
{
    int a = 100;
    
    printLRef(a);
    
    printRRef(a);	//ERROR
    
    return 0;
}

 

printLRef는 L-Value를 printRRef는 R-Vlaue를 받아주는데 main함수에서 printRRef(a);는 L-Value를 넘겨주기 때문에 컴파일 에러가 난다. 해당 에러를 고쳐주기 위해서는 std::move() 함수를 통해서 R-Vlaue로 변환을 한 후에 Argument로 넘겨줘야 한다. 

 

Template 함수를 이용해 위와 같은 함수를 만든다고 가정을 해보자. Template을 사용하는 이유는 어떠한 Type을 Argument로 주어도 Compile-time에 해당 타입에 맞는 함수를 만들어 줘야 한다. Template 함수에서 Reference를 이용해서 Argument를 넘겨주는 방법은 아래와 같다. 

 

#include <iostream>

template<typename T>
void printVar(T&& a)
{
    std::cout << a << std::endl;
}

int main()
{
    int a = 100;
    
    printVar(a);
    
    printVar(std::move(a));
    
    return 0;
}

 

위 코드에서 Template함수의 Parameter는 T&& R-Value를 받게 되어있다. 하지만 main함수에서 L-Value를 Argument로 넘겨주었는데 에러 없이 컴파일이 문제없이 되었다. 이유는 Template에서 T&&는 R-Value Reference가 아니라 Forward Reference라고 부른다. 당연하게 이는 L-Value Reference가 되기도 하고 R-Value Reference가 되기도 한다. 

 

더 자세히 위 내용을 이해하기 위해 Compiler Explorer를 이용해서 어셈블리 코드를 보자. 아래는 위 코드를 어셈블리 코드로 변환시킨 화면이다. 

 

 

어셈블리 코드를 보게 되면 컴파일러가 함수의 Argument를 보고 맞는 타입의 함수를 만든 것을 확인할 수 있다. L-Value reference를 Argument로 받은 함수는 T가 int&이고 함수의 Parameter는 int&가 된다. R-Value Reference를 Argument로 받은 함수는 T가 int고 Parameter는 int&&가 된다. 

 

R-Value Reference를 사용하는 이유 중 하나는 소유권을 뺏어오는 목적이 있다. 위와 같은 L-Value와 R-Value를 둘 다 넘겨줄 수 있는 Template 함수에서 소유권은 어떻게 취득하는지 알아보자. 

 

 

Perfect Forwarding

 

#include <iostream>

template<typename T>
void printVar(T&& a)
{
    std::string localVar{ std::forward<T>(a) };
    std::cout << localVar <<  std::endl;
}


int main()
{
    std::string str = "pretending";

    printVar(str);
    printVar(std::move(str));

    return 0;
}

 

main 함수에서 string 객체인 str을 선언하고 template 함수의 Argument로 각각 L-Value Reference와 R-Value Reference로 호출하였다. Template 함수 내에서 localVar이라는 string 객체를 만들어주고 std::forward <T>(a);로 초기화를 해주었는데 이때 std::forward <T>(a);에서 만약 a가 L-Value Reference라면 그대로 L-Vlaue Reference로 놔두게 되고 R-Vlaue Reference라면 그래도 R-Value Reference로 놔두게 된다. 

 

 

Ref.

https://www.youtube.com/watch?v=Ifo2RtSzqvQ&list=PLDV-cCQnUlIb2oezNpNTmxiiX_NibMrlO&index=3 

반응형
반응형

학기 중 Template을 이용하여 코드를 짜는 과제가 있었는데 정말 template을 어떻게 사용하는지 모르겠어서 진짜 이상하게 코드를 짰던 기억이 난다. (물론 코드는 짜서 점수는 받았지만...) 지금 다시 template을 제대로 처음부터 다시 공부를 해본다. 

 

 

Template (템플릿)

 

Tempalate은 아래와 같이 많은 종류의 Template이 존재한다. 

  • Function template
  • Class Template
  • Alias Template
  • Variable Template

Template이란 변수의 Type을 정해주지 않고 필요한 Type을 Compile-time에 정의를 해서 사용을 한다. 이러한 특성은 우리가 tyep 때문에 Function Overloading을 해야 할 경우 가장 유용하게 쓰인다

 

뭔 뜻인지 이해가 안 될 수 있다. 코드를 통해서 이해를 해보자. 

 

#include <iostream>

int add(int a, int b)
{
    return a + b;
}

float add(float a, float b)
{
    return a + b;
}

double add(double a, double b)
{
    return a + b;
}


int main()
{
    std::cout << add(1, 2);
    std::cout << add(1.2f, 2.3f);
    std::cout << add(1.3, 1.4);
}

 

위와 같이 두 수를 더해주는 함수를 만드려고 한다. integer타입, float타입 그리고 double타입에 대한 Funciton Overloading에 의해서 3개의 함수가 만들어져야 한다.  하지만 이렇게 타입이 더 늘어나게 되면 많은 함수가 만들어져야 하기 때문에 Template을 이용해서 함수를 만들 수 있다

 

 

Function Template (함수 템플릿)

 

Template을 이용하여 추상적인 Type을 만들고 컴파일 타임에 각 타입에 맞는 함수가 생성된다고 생각하면 된다. 

 

#include <iostream>

template<typename T>
T add(T a, T b)
{
    return a + b;
}


int main()
{
    std::cout << add<int>(1, 2);
    std::cout << add<float>(1.2f, 2.3f);
    std::cout << add<double>(1.3, 1.4);
}

위와 같이 template을 이용하여 함수를 만들게 되면 Function  Overloading을 이용하여 만든 것보다 더 짧고 명확하게 코드를 짤 수 있다. 

 

Template으로 만들어진 함수는 Compile-time에 정의된다. 이를 확인하기 위해 compiler explorer을 통해서 확인해 보자. 

 

 

template으로 만들어진 함수만 어셈블리로 변 활했을 경우는 어떤 어셈블리 코드도 만들어지지 않는다. 

 

 

이와 같이 원래는 code로만 존재를 하다가 main함수에서 해당 template function을 사용을 할 때 그 Type에 맞는 함수가 Compile이 되면서 함수가 만들어진다. 

 

여기서 궁금한 것은 모든 자료형에 대해서 적용이 가능한가?

 

두 개의 const char*형의 + 연산을 한다고 가정을 해보자. 아래 코드와 같이 작성이 가능하다. 

 

#include <iostream>

template<typename T>
T add(T a, T b)
{
    return a + b;
}


int main()
{
    std::cout << add<const char*>("aaa", "bbb");
}

 

위와 같은 코드를 컴파일이 안된다. 이유는 const char*형에 대해서 +연산이 정의되어있지 않기 때문이다. Template Function은 모든 것이 만들어져 있는 것이 아니라 넘겨지는 Type에 따라서 그때 Compile이 되게 된다

 

 

이번 포스트는 Template에 대해서 간단하게 알아보았다.

 

 

Ref.

https://nocodeprogram.com/lecture/1/42793/

반응형
반응형

 

Shared Pointer를 이용하여 Circular Reference를 하여 Memory Leak이 발생할 수 있다고 공부를 했다. Weak Pointer의 개념에 대해서 공부하고 이를 이용하여 Circular Reference문제를 해결해 보자. 

 

 

Weak Pointer (위크 포인터)

 

Weak Pointer는 말 그대도 약간 포인터이다. Shared Pointer를 참조하는 용도로 사용을 하게 되고 Weak Pointer가 Shared Pointer를 참조를 하게 되면 Weak Count가 증가하게 된다. Weak Pointer를 이용하여 오브젝트의 리소스를 접근할 수 없다. Weak Pointer를 사용을 하려면 반드시 Shared Pointer형을 반환해 주는 lock() 메서드를 이용하여 Shared Pointer로 변환을 해야 되는데 이때 참조하는 Shared Pointer의 Strong Count가 증가하게 된다는 것을 잊으면 안 된다. 

 

아래는 Weak Pointer가 선언되고 Shared Pointer를 참조하는 코드이다. 

 

#include <iostream>
#include <memory>

class Cat
{
public:
    Cat(std::string name) : mName{ name }
    {
        std::cout << mName << " cat constructor" << std::endl;
    }
    virtual void speak() const
    {
        std::cout << "Hi?" << std::endl;
    }
    ~Cat()
    {
        std::cout << mName << " cat destructor" << std::endl;
    }
    std::shared_ptr<Cat> mVar;
private:
    std::string mName;
};


int main()
{
    std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
    std::weak_ptr<Cat> weak_kitty = kitty;
    //weak pointer 선언 및 shared pointer kitty참조
    std::cout << "count : " << kitty.use_count() << std::endl;
    
    return 0;
}

 

위 코드를 보면 Shared Pointer kitty를 선언하고 Cat 오브젝트를 생성하였다. 그다음 Weak Pointer를 선언하여 kitty가 가리키고 있는 오브젝트를 참조를 하였다. 그다음 kitty가 가리키고 있는 오브젝트의 count를 출력한 결과 1이 출력이 되었다. 

 

만약 Weak Pointer가 참조하고 있는 오브젝트의 리소스를 사용하고 싶다면 lock() 메서드를 통해 Shared Pointer를 반환받을 수 있다. 아래 코드는 Weak Pointer의 lock() 메서드를 통해 새로운 Shared Pointer를 생성하여 리소스를 사용하는 코드이다. 

 

#include <iostream>
#include <memory>

class Cat
{
public:
    Cat(std::string name) : mName{ name }
    {
        std::cout << mName << " cat constructor" << std::endl;
    }
    virtual void speak() const
    {
        std::cout << "Hi?" << std::endl;
    }
    ~Cat()
    {
        std::cout << mName << " cat destructor" << std::endl;
    }
    std::shared_ptr<Cat> mVar;
private:
    std::string mName;
};


int main()
{
    std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
    std::weak_ptr<Cat> weak_kitty = kitty;
    
    if(const auto shared_kitty = weak_kitty.lock())
    {
        shared_kitty->speak();
    }
    else
    {
        std::cout << "pointing nothing\n";
    }
    
    return 0;
}

 

위 코드는 if문 조건문에 const auto shared_kitty에 Weak Pointer weak_kitty의 lock() 메서드를 통하여 Shared Pointer를 반환해주었다. 만약 가리키고 있는 Shared Pointer가 메모리 해제가 된 경우 empty shared_ptr를 반환해준다. 위 경우 weak_kitty가 가리키고 있는 kitty 오브젝트는 마메로 해제가 안되었기 때문에 해당 shared_pointer를 반환한다. if 조건문을 scope로 가지는 shared_kitty가 kitty 오브젝트를 가리키고 있기 때문에 kitty의 count가 2로 증가하게 되고 조건문이 끝나고 다시 1로 감소된다. 

 

그렇다면 아래 코드를 보자.

 

#include <iostream>
#include <memory>

class Cat
{
public:
    Cat(std::string name) : mName{ name }
    {
        std::cout << mName << " cat constructor" << std::endl;
    }
    virtual void speak() const
    {
        std::cout << "Hi?" << std::endl;
    }
    ~Cat()
    {
        std::cout << mName << " cat destructor" << std::endl;
    }
    std::shared_ptr<Cat> mVar;
private:
    std::string mName;
};


int main()
{
    std::weak_ptr<Cat> weak_kitty;
    {
        std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
        weak_kitty = kitty;
    }
    
    if(const auto shared_kitty = weak_kitty.lock())
    {
        shared_kitty->speak();
    }
    else
    {
        std::cout << "pointing nothing\n";
    }
    
    return 0;
}

 

위 코드는 Weak Pointer는 main함수 scope에 선언이 되었고 Shared Pointer인 kitty는 Curly brace scope에 선언이 되었다. 먼저 괄호 안에 선언된 kitty에 Cat 오브젝트를 생성하고 main함수 scope에 있는 weak_kitty를 kitty오브젝트를 참조하게 했다. 괄호를 벗어나고 if문에서 weak_kitty의 lock() 메서드로 Shared Pointer를 반환하려고 했지만 kitty 오브젝트는 이미 괄호 안에서 할당 해제가 되었기 때문에 empty Share Pointer가 반환되어 pointing nothing을 출력하게 된다. 

 

 

Circular Reference (순환 참조) 해결 

 

#include <iostream>
#include <memory>

class Cat
{
public:
	Cat(std::string name) : mName{ name }
	{
		std::cout << mName << " cat constructor" << std::endl;
	}
	~Cat()
	{
		std::cout << mName << " cat destructor" << std::endl;
	}
	std::shared_ptr<Cat> mVar;
private:
	std::string mName;
};

int main()
{
	std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
	std::shared_ptr<Cat> nabi = std::make_shared<Cat>("nabi");

	kitty->mVar = nabi;
	nabi->mVar = kitty;

	std::cout << "kitty count : " << kitty.use_count() << std::endl;
	std::cout << "nabi count : " << nabi.use_count() << std::endl;
}

 

저번 포스팅에서 다루었던 오브젝트 내부의 Shared Pointer가 서로 다른 오브젝트를 가리키게 되어 count가 줄지 않아 메모리 해제가 안되었던 문제를 Weak Pointer로 해결할 수 있다. 단순하게 클래스 내부의 Shared Pointer를 Weak Pointer로 교체를 하면 문제는 해결된다. 

 

#include <iostream>
#include <memory>

class Cat
{
public:
	Cat(std::string name) : mName{ name }
	{
		std::cout << mName << " cat constructor" << std::endl;
	}
	~Cat()
	{
		std::cout << mName << " cat destructor" << std::endl;
	}
	std::weak_ptr<Cat> mVar;
private:
	std::string mName;
};

int main()
{
	std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
	std::shared_ptr<Cat> nabi = std::make_shared<Cat>("nabi");

	kitty->mVar = nabi;
	nabi->mVar = kitty;

	std::cout << "kitty count : " << kitty.use_count() << std::endl;
	std::cout << "nabi count : " << nabi.use_count() << std::endl;
}

 

이렇게 코드를 바꾸게 되면 count는 증가하지 않고 main함수 스코프를 가진 kitty와 nabi가 할당 해제가 되면서 각 오브젝트로 메모리 해제가 된다. 

 

 

Ref.

 

https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg

https://en.cppreference.com/w/cpp/memory/weak_ptr

반응형
반응형

Unique Pointer에 이어서 Smart Pointer의 한 종류인 Shared Pointer에 대해서 정리한다. 

 

 

Shared Pointer (쉐어드 포인터)

 

이름에서도 알 수 있듯이 해당 포인터는 Exclusive Ownership을 가지는 Unique pointer와 다르게 Shared Ownership을 가지게 된다. 그 말은 하나의 오브젝트를 여러 개의 포인터가 가질 수 있다. 하지만 모든 스마트 포인터는 RAII콘셉트를 제공을 해야 한다. Shared Pointer는 여러 개의 포인터가 가리킴에도 불구하고 어떻게 오브젝트의 메모리 할당을 해제시키는지 알아보자. 

 

아래 코드는 쉐어드 포인터를 선언하는 코드이다. 

class Dog
{
public:
    Dog() : mAge { 0 }
    {
        std::cout << "dog Constructor" << std::endl;
    }
    ~Dog()
    {
        std::cout << "Dog destructor" << std::endl;
    }
private:
    int mAge;
}

int main()
{
    std::shared_ptr<Dog> dogPtr = std::make_shared<Dog>();
}

위 코드에서 dogPtr이라는 쉐어드 포인터를 생성하고 Dog객체를 할당하였다. 위 과정에서 해당 포인터의 scope단위는 main함수이고 객체가 생성이 되면서 Constructor를 부르게 되고 main함수가 종료되면서 자동적으로 Destrucotr를 호출하게 된다. 

 

하지만 쉐어드 포인터의 특성으로 다른 쉐어드 포인터가 하나의 오브젝트를 가리킬 수 있다. 아래의 코드는 여러 개의 쉐어드 포인터가 하나의 오브젝트를 가리키는 예이다. 

int main()
{
    std::shared_ptr<Dog> dogPtr = std::make_shared<Dog>();
    std::shared_ptr<Dog> dogPtr1 = dogPtr;
}

위와 같은 경우 Unique Pointer와 다르게 하나의 오브젝트를 두 개의 포인터가 가리키게 된다. 만약에 여기서 끝이라면 일반 포인터와 다를 바가 없다. 일반 포인터와 다른 점은 가리키는 메모리 공간에 몇 개의 포인터가 오브젝트를 가리키는지 Count를 한다. Count 중에서도 Strong Count와 Weak Count가 있는데 쉐어드 포인터가 가리키게 되면 Strong Count가 올라가고 오브젝트는 Strong Count가 0일 때 메모리를 해제를 결정하게 된다. Weak Pointer가 가리키게 되는 경우는 Weak Count가 올라가지만 메모리를 해제하기 위해서 참고되지는 않는다.

 

Shared Pointer은 개발자가 Rsource의 Life Cycle을 고려하지 않고(메모리 해제를 고려하지 않고) 한 오브젝트의 Ownership을 여러 Scope에서 공유가 가능하게 만들어준다. 

 

하지만 이러한 특성 때문에 의도치 않은 Memory Leak이 일어날 수 있다

 

아래 코드는 Shared Pointer 사용함에 있어서 Memory Leak이 일어나는 가장 쉬운 예이다. 

 

#include <iostream>
#include <memory>

class Cat
{
public:
	Cat(std::string name) : mName{ name }
	{
		std::cout << mName << " cat constructor" << std::endl;
	}
	~Cat()
	{
		std::cout << mName << " cat destructor" << std::endl;
	}
	std::shared_ptr<Cat> mVar;
    
private:
	std::string mName;
};

int main()
{
	std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
	kitty->mVar = kitty;
	std::cout << kitty.use_count() << std::endl;
}

먼저 클래스 내에 Shared Pointer mVar를 추가로 생성하였다. main함수에서 Shared Pointer kitty를 선언하고 오브젝트를 생성하고 오브젝트 내의 Shared Pointer mVar를 오브젝트 자신인 kitty를 가리키게 만들었다. 그다음 kitty 오브젝트의 Strong Count를 확인하기 위해 use_count()를 이용해 출력을 한 결과 2가 출력이 된다. 이 경우  Memory Leak이 발생하게 되는데 아래 그림을 통해 알아보자.

 

Shared Pointer인 kitty는 main함수 scope가 종료됨에 따라 메모리 해제가 된다. kitty가 사라지고 오브젝트의 Count가 2에서 1로 줄게 된다. Shared Pointer가 해제되고 오브젝트 또한 자동으로 해제되는 것을 기대했지만 오브젝트 내부의 Shared Pointer가 오브젝트를 아직 가리키고 있기 때문에 main함수가 종료됐음에도 불구하고 메모리 해제가 안되어 Memory Leak이 일어난 경우이다. 

 

위 예는 Memory Leak이 일어날 수 있다는 것을 보여주기 위한 예로 실제 많이 발생되지 않는다. 실제로 자주 발생되는 Memory Leak에 대한 코드는 아래와 같다. 


Circular Reference (순환 참조)

 

#include <iostream>
#include <memory>

class Cat
{
public:
	Cat(std::string name) : mName{ name }
	{
		std::cout << mName << " cat constructor" << std::endl;
	}
	~Cat()
	{
		std::cout << mName << " cat destructor" << std::endl;
	}
	std::shared_ptr<Cat> mVar;
private:
	std::string mName;
};

int main()
{
	std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
	std::shared_ptr<Cat> nabi = std::make_shared<Cat>("nabi");

	kitty->mVar = nabi;
	nabi->mVar = kitty;

	std::cout << "kitty count : " << kitty.use_count() << std::endl;
	std::cout << "nabi count : " << nabi.use_count() << std::endl;
}

 

순환 참조란 위 코드와 그림처럼 클래스 내부에서 Shared Pointer로 다른 클래스가 서로 가리키는 것을 의미한다.  kitty 오브젝트와 nabi 오브젝트가 Shared Pointer kitty와 nabi에 의해서 가리키게 되고 내부의 mVar Shared Pointer가 서로의 오브젝트를 가리키게 되면 Shared Pointer가 main함수에서 메모리 해제가 되어도 각 오브젝트의 Count는 1로 메모리 해제가 안 되는 것을 말한다. 

 

위와 같은 Memory Leak을 해결하기 위해 Weak_ptr을 이용하여 Circular Reference를 구현할 수 있다. 다음 포스팅은 Weak_Ptr에 대해서 공부할 예정이다. 

 

 

Ref.

https://www.youtube.com/watch?v=tg34hwP0P0M&list=PLDV-cCQnUlIbOBiPvBaRPezOLArubgZbQ&index=4 

반응형

+ Recent posts