C++ Tricks

4 minute read

Published:

LevelDB를 하나하나 뜯어보면서 공부하고 있습니다. 이 코드를 보기 시작하면서 C++에 신기한 기능들이 정말 많다는 것을 알게 되었고, 코딩테스트 준비를 하면서 사용한 C++의 여러 기능들은 정말 수박 겉핥기였다는 것을 새삼 느끼고 있습니다. 또, 이미 아는 내용도 정말 세련되게 사용하는 방법들이 있다는 것도 깨닫게 되었습니다.

이 포스팅에서는 LevelDB의 코드들을 보면서 배운 새로운 내용들, 복습할 내용들, 또는 몰랐던 사용법을 알게 된 부분들을 정리하고자 합니다. 각 주제별로 최대한 간단하게 적어보고자 하는데, 내용이 길어지면 별도의 포스팅으로 분리하거나 레퍼런스 링크를 걸어두도록 하겠습니다.

Export Macro

매크로를 사용해 클래스, 객체, 혹은 함수의 export 혹은 import를 환경에 알맞게 자동으로 처리할 수 있었습니다. 내용이 길어져서 다른 포스팅으로 정리했습니다.

Const Function

출처: https://docs.microsoft.com/ko-kr/cpp/cpp/const-cpp?view=msvc-170

코드를 살펴보면 다음과 같은 모양새를 종종 볼 수 있습니다.

const char* data() const { return data_; }

어떤 클래스의 data_ 값을 반환하는 함수입니다. 처음 나오는 const char*이야 반환되는 data_의 값이 수정될 수 없는 상수라는 것을 나타냅니다. 그리고 함수명 다음에 나오는 두 번째 const는 이와 비슷한데, 이 함수가 호출하는 개체를 수정하지 못하도록 막는 기능을 합니다. 예를 들어서 다음과 같은 간단한 클래스와 메인 함수를 살펴보겠습니다.

#include <iostream>

class Person
{
    public:
        void change_name(std::string name) {
            name_ = name;
        }
        void read_name() const {
            std::cout << name_ << std::endl;
        }
    private:
        std::string name_;
}

이름만 가지는 사람 클래스입니다. 단순한 예시를 위해 생성자와 소멸자 등은 생략했습니다. 이름을 바꾸는 change_name 함수는 const 키워드가 없기 때문에 name_을 바꿀 수 있습니다. 하지만 read_name 함수는 const 키워드 때문에 name_을 바꿀 수 없습니다. 만약 다음과 같이 read_name 함수를 바꾼다면,

void read_name() const {
    name_ = "remove name";
    std::cout << name_ << std::endl;
}

이런 에러가 나오면서 컴파일을 할 수 없게 됩니다.

no operator "=" matches these operands

Operator Overloading

출처: https://docs.microsoft.com/ko-kr/cpp/cpp/operator-overloading?view=msvc-170

연산자 오버로드는 일부 연산자를 전역적으로, 혹은 클래스 단위로 다시 설정할 수 있도록 하는 기능입니다. 오버로드되는 연산자는 함수로 구현됩니다. 즉, + 연산자를 오버로드하려면 operator+라는 함수를 정의하는 것입니다.

연산자 오버로드의 형식은 다음과 같습니다.

operator<operator-symbol>(parameter-list)

이게 무슨 의미인지 다음 코드를 보면서 설명하겠습니다. 두 개의 정수가 포함된 Tuple 클래스입니다.

#include <iostream>

class Tuple
{
    public:
        Tuple(int x, int y) : x_(x), y_(y) {}
        Tuple operator+( Tuple &other ); // + 연산자 오버로드를 선언합니다. 
        void Display() { std::cout << x_ << " , " << y_ << std::endl; }
    private:
        int x_, y_;
};

Tuple Tuple::operator+( Tuple &other ) { // 연산자 오버로드는 함수로 구현됩니다. 
    return Tuple( x_ + other.x_, y_ + other.y_ );
}

int main() {
    Tuple a = Tuple(1, 5);
    Tuple b = Tuple(3, 8);
    Tuple c = Tuple(0, 0);

    c = a + b;
    c.Display();

    return 0;
}

일반적으로 두 개의 튜플(tuple) (1,5)와 (3,8)이 있을 때 이들을 더한다면 우리는 각각의 자리를 더해서 (4,13)이 된다고 쉽게 상상할 수 있습니다. 하지만 이렇게 사용자 정의된 클래스의 값을 더할 때, 즉 + 연산자를 사용할 때 그 규칙을 정해놓지 않으면 컴파일러로서는 어떻게 연산을 해야 하는지 알아낼 수 없습니다. 따라서, 위 예시에서 operator+ 선언과 정의를 제거한다면 다음과 같은 에러가 발생합니다.

error: no match for ‘operator+’ (operand types are ‘Tuple’ and ‘Tuple’)

‘나는 TupleTuple을 더하는 방법을 모르는데 어쩌라는거냐’라는 뜻이죠. 그래서 위 예시와 같이 연산자 오버로드를 통해 Tuple 클래스에 대한 + 연산자를 재정의하면 컴파일러도 ‘아, 각각의 자리를 더하라는 의미구나’라는 것을 이해하고 정상적인 연산을 수행할 수 있게 됩니다.

Inline Functions

출처: https://docs.microsoft.com/ko-kr/cpp/cpp/inline-functions-cpp?view=msvc-170

코드에서 다음과 같은 인라인 함수를 가끔 만날 수 있습니다.

inline bool operator!=(const Slice& x, const Slice& y) { return !(x == y); }

바로 위에서 살펴본 연산자 오버로드의 예시이기도 합니다만, 여기서 inline 키워드가 의미하는 바는 컴파일러에게 모든 함수 호철 건에 대해 함수 정의 내에서 코드를 대체하라는 지시를 내리는 것입니다. 일반적으로 함수를 호출할 때 함수가 위치한 레이블로 점프해 실행합니다. 하지만 inline 키워드를 사용하면 컴파일러가 해당 함수의 코드를 함수가 호출되는 위치로 끌고 와서 컴파일을 해버리는 것입니다.

예를 들어서 다음과 같은 코드가 있다고 한다면,

#include <iostream>

inline void print() {
    std::cout << "Hello, world!" << std::endl;
}

int main() {
    print();
    return 0;
}

메인 함수는 이렇게 치환되어 실행될 것입니다.

int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

inline 키워드에 대해 알아두면 좋은 내용은 다음과 같습니다.

  1. inline 키워드가 있다고 항상 코드로 치환되는 것은 아닙니다. inline 키워드는 컴파일러에게 주는 힌트일 뿐, 컴파일러가 비효율적이라고 판단하면 인라인을 포기할 수 있습니다. 반대로, inline 키워드가 없어도 컴파일러 판단 하에 인라인 처리를 할 수도 있습니다.
  2. #define, 즉 매크로와 비슷하다고 느껴질 수 있습니다. 차이점은 여러 가지 있는데, 타입체크를 하지 않는다는 점, 처리의 주체가 전처리기와 컴파일러로 다르다는 점, 무시할 수 없는 매크로와 다르게 inline은 무시 가능하다는 점 등이 있습니다.