C++은 끊임없이 진화하고 있습니다. 과거의 복잡하고 위험했던 코드들은 이제 우아하고 안전한 현대적 문법으로 대체되고 있죠. 오늘은 C++ 엔지니어라면 반드시 알아야 할, 실무의 질을 바꾸는 핵심 문법 6가지를 소개합니다.

 

기능 (Feature) 도입 표준 주요 역할
구조화된 바인딩 C++17 데이터 컨테이너의 직관적 해체
std::views / Ranges C++20 선언적 데이터 처리 및 파이프라인
std::format C++20 타입 안전한 고속 포맷팅
std::jthread C++20 RAII 기반의 안전한 스레드 제어
consteval C++20 컴파일 타임 연산 강제 (Immediate Functions)
std::is_constant_evaluated C++20 컴파일/런타임 문맥 판별
std::expected C++23 모나딕 기반의 우아한 에러 핸들링

 

1. std::views와 Ranges (C++20) — "반복문 코드 다이어트"

복잡한 필터링과 변환 로직, 이제 for문 대신 파이프라인 연산자(|)로 한 줄에 해결하세요. 원본 컨테이너를 복사하지 않는 'View' 방식이라 성능 오버헤드도 없습니다.

 

기본 예제

#include <vector>
#include <ranges>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6};

    // 짝수 추출 -> 10 곱하기
    auto result = nums | std::views::filter([](int n) { return n % 2 == 0; })
                       | std::views::transform([](int n) { return n * 10; });

    for (int n : result) std::cout << n << " "; // 결과: 20 40 60
}

 

view를 이용한 이미지의 이진화 예제

#include <vector>
#include <ranges>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>

#pragma comment(lib, "opencv_world341d.lib")


int main() {

    cv::Mat mat, matGray, matGrayView, matGrayColor, matGrayViewColor;
    mat = cv::imread("D:\\capture.bmp");

    cv::cvtColor(mat, matGray, cv::COLOR_BGR2GRAY);
    matGrayView = matGray.clone();

    std::span<uchar> data_span(matGrayView.data, matGrayView.total());
    // 짝수만 골라서 10을 곱한 뒤 출력하기
    auto result = data_span | std::views::transform([](uchar& pixel) { return pixel = (pixel >= 50) ? 255 : 0; });

    // 4. 파이프라인 구동 (평가 수행)
        // Views는 지연 평가(Lazy Evaluation)되므로 루프를 돌 때 실제로 연산이 일어납니다.
    for (uchar pixel : result) {
        // 이 루프를 돌고 나면 원본 img.data가 직접 수정됩니다.
    }

    cv::cvtColor(matGray, matGrayColor, cv::COLOR_GRAY2BGR);
    cv::cvtColor(matGrayView, matGrayViewColor, cv::COLOR_GRAY2BGR);
    std::vector<cv::Mat> space_synchronized_images = { mat, matGrayColor, matGrayViewColor };

    // 6. 나란히 가로로 붙이기
    cv::Mat combined;
    cv::hconcat(space_synchronized_images, combined);

    cv::imshow("TEST", combined);
    cv::waitKey(10000);

}


 

2. 구조화된 바인딩 (C++17) — "가독성 깡패"

std::pair나 struct의 멤버를 하나씩 꺼내 쓰느라 지저분했던 코드, 이제는 구조화된 바인딩으로 직관적으로 이름을 붙여 사용하세요.

 
#include <vector>
#include <ranges>
#include <iostream>
#include <map>
#include <string>
#include <print>


int main() {

    std::map<int, std::string> users = { {10, "Test"}, {11, "My"}, };

    // 옛날엔 이터레이터 돌려서 it->first, it->second로 접근했지?
    for (const auto& [id, name] : users) {
        // id와 name을 일반 변수처럼 바로 사용 가능!
        std::println("Map {}->{}", id, name);
    }
}

3. std::format (C++20) — "형식 안정성이 보장된 printf"

std::cout의 스트림 연산자 << 지옥은 이제 그만. 파이썬이나 C#처럼 간결하고 안전한 포맷팅을 지원합니다.

 

 

#include <format>
#include <iostream>

std::string s = std::format("안녕 오빠, 여기는 Visual Studio {}이야! By {}.", 2026, "제니");
std::cout << s;

 

이 format 기능을 이용하기 위해서는 프로젝트 속성에서 C++ 표준을 바꿔줘야 한다.

 

4. std::jthread (C++20) — "자원 해제 스트레스 해소"

std::thread의 고질적인 문제였던 .join() 누락 문제를 소멸자에서 자동으로 해결해 줍니다. 멀티스레드 설계 시 발생하던 크래시를 근본적으로 차단하세요.

#include <iostream>
#include <thread>
#include <chrono>

// 인자에 stop_token을 넣어두면 컴파일러가 알아서 매칭해 줌
void infinite_loop(std::stop_token token) {
    while (!token.stop_requested()) { // 외부에서 멈추라고 했는지 체크
        std::cout << "하드웨어 데이터 수집 중...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    std::cout << "스레드 안전하게 종료!\n";
}

int main() {
    std::jthread jt(infinite_loop);

    std::this_thread::sleep_for(std::chrono::seconds(2));

    // 외부에서 "이제 그만!" 신호 보내기
    // 호출 즉시 token.stop_requested()가 true가 됨
    jt.request_stop(); 
}

5. consteval & is_constant_evaluated() (C++20) — "컴파일 타임의 끝판왕"

런타임 비용을 0으로 만드는 마법입니다. consteval은 함수 실행을 컴파일 타임으로 강제하여, 연산 결과를 상수처럼 즉시 박아버립니다.

 
consteval int square(int n) { return n * n; }
constexpr int val = square(10); // 컴파일 타임에 100으로 계산 완료!

6. std::expected (C++23) — "에러 핸들링의 혁신"

예외(Exception)와 에러 코드 사이에서 갈팡질팡하던 시대는 끝났습니다. 성공하면 '값'을, 실패하면 '에러 객체'를 반환하는 안전한 컨테이너를 사용하세요.

 
#include <iostream>
#include <expected>
#include <string>

// 에러 종류 정의
enum class PrintError { OutOfMaterial, FileCorrupted, HardwareFailure };

void handlePrintError(const PrintError& e) {
    if (e == PrintError::OutOfMaterial) std::cerr << "[LOG] 에러: 실리콘 원료 부족!\n";
    else if (e == PrintError::FileCorrupted) std::cerr << "[LOG] 에러: 파일 데이터 손상!\n";
    else std::cerr << "[LOG] 에러: 하드웨어 손상!\n";
}

// 템플릿 헬퍼: 에러 처리의 공통 로직을 캡슐화
template <typename T, typename E, typename SuccessFunc>
void executeTask(std::expected<T, E>&& res, SuccessFunc&& onSuccess) {
    // 마지막 체이닝 결과를 void로 캐스팅하여 '의도적으로 무시함'을 알림
    static_cast<void>(
        res.and_then([&](const T& val) -> std::expected<void, E> {
        onSuccess(val);
        return {};
    })
        .or_else([&](const E& err) -> std::expected<void, E> {
        handlePrintError(err);
        return std::unexpected(err);
    })
    );
}

// 3D 프린터의 상태를 나타내는 구조체
struct Sample { std::string id; int quality; };

// 샘플 제작 함수: 성공 시 Sample, 실패 시 PrintError 반환
std::expected<Sample, PrintError> createSample(int materialAmount, bool isFileValid) {
    if (materialAmount < 10) return std::unexpected(PrintError::OutOfMaterial);
    if (!isFileValid) return std::unexpected(PrintError::FileCorrupted);

    return Sample{ "Jenny_v0.1", 100 }; // 성공!
}

int main() {
    // 1. 샘플 제작 시도
    executeTask(createSample(20, true), [&](const Sample& s)
    {
        std::cout << "성공: " << s.id << " 제작 시작!\n";
    });

    return 0;
}

 

마치며

이 문법들은 단순히 코드를 짧게 만드는 것을 넘어, 더 안전하고 예측 가능한 프로그램을 만드는 데 목적이 있습니다. 10년 전의 C++과 지금의 C++은 완전히 다른 언어라 할 만큼 발전했습니다. 오늘 당장 프로젝트에 하나씩 적용해 보세요!

여러번 충전할 수 있는 2차전지의 수명은 내부저항과 밀접한 관계가 있다.

내부저항이 낮을 수록 새거고 높으면 방전이 금방되어 수명이 거의 다 된 상태가 된다.

DIY카페나 방전된 배터리팩에서 배터리의 상태를 확인하기 위해서는 이런 테스터기가 필요하다.

가장 정확한 것은 직접 방전을 해 보는 방법이다.

방전기라는 장비가 있는데 비싸고 모두 방전되기 까지 시간도 오래 걸리기 때문에 성격급한 나랑은 맞지 않다.

그래서 제미나이에게 배터리 성능을 어떻게 확인할 수 있냐 물어보니 내부저항을 측정하면 된다고 하더라

그래서 이 리튬배터리의 내부 저항을 측정하는 기기를 만들었다.

 

재료는 아래와 같다.

부품명 수량 사양 / 설명 비고
Arduino Nano 1 ATmega328P (V3.1) 메인 MCU
INA219 모듈 1 DC Current/Voltage Sensor I2C 인터페이스
OLED Display 1 0.96” SSD1306 (128×64) I2C 인터페이스
30N06L MOSFET 1 N-Channel Logic Level MOSFET 부하 스위칭용
DA-2 5V Relay 1 5V DC 릴레이 배터리 보호 및 차단
BD237 1 NPN Transistor (TO-92) 릴레이 구동용
Active Buzzer 1 5V 액티브 부저 상태 알림용
시멘트 저항 2 7Ω 5W 병렬 연결 (3.5Ω / 10W)
1kΩ 저항 2 1/4W 탄소피막 또는 금속피막 MOSFET/Tr Base 보호용
10kΩ 저항 1 1/4W 저항 MOSFET Gate 풀다운용
5MΩ 저항 1 1/4W 고저항 배터리 (-)단자 접지용
1N4148 1 Switching Diode 릴레이 역기전력 방지용

 

 

회로도

 

 

 

기판은 만능기판을 만들고 난잡한 점프선의 문제를 해결하기 위해 구리 테이프를 회로선 처럼 잘라서 구성해서 납땜했다.

AI의 잘못된 답변으로 18650 반대로 꽂아도 INA219 안태워 먹게끔 기기를 업그래이드 하느라 INA219 3개를 태워 먹었다.

그래서 결국 배터리 역삽입에 대한 대응은 없는 것으로 하고 최종버전이 위 회로도와 같이 되었다.

 


내부저항 측정 원리

공식

 

설명

 

 

아래 이미지는 작업 이미지

 

역삽입 인식 기능을 위해 부품 수가 많아져서 2층 pcb 구조로 작업을 했다. (현재 최종 버전)

 

AI의 도움으로 본 배터리 내부저항 테스터기 핵심인 아두이노 코드를 완성하였고 몇차례 수정/보완 작업을 거쳐 완성하였다.

 

 

 

내가 가입해 있는 DIY 카페에서 18650 배터리 50개가 들어있는 36V 13A짜리 EVE 2600 킥보드에 들어가는 배터리 팩을 경매로 낙찰 받았다.

36V짜리면 10S(10개 직렬연결)이고 50개 들어있으니 이10개 5개 세트가 서로 병렬로 연결된 구조이다.

즉, 5개 병렬로 연결된 하나의 세트가 10개 세트 직렬로 연결된 구조.

배터리팩은 이렇게 생겼다.

 

 

나사를 풀고 안에 숨어있는 18650 배터리들을 세상 밖으로 꺼낸다.

 

배터리를 묶고 있는 저 테이프들을 제거하고 직렬 연결된 배터리들을 분리한다.

 

 

배터리에 붙어있는 저 니켈 스트립을 모두 떼어 내고 각각 하나의 배터리로 분리 해 내고 나면 이런 모습이다.

 

 

 

저 니켈 스트립을 떼고 나면 분명 이렇게 끝부분에 날카로운 잔해물이 붙어있다.

 

이걸 제거해야 나중에 재사용 할 때 문제가 없고 저거 때문에 상처가 생길일도 없어진다.

 

 

배터리를 감싸던 프레임과 테이프등은 버리고 충전단자와 퓨즈등은 나중을 위해 보관해 놓는다.

 

 

배터리 양 끝에 붙어있는 저런 날카로운 니켈 스트립은 그라인더를 이용해서 모두 제거한다.

 

이상 끝.

+ Recent posts