ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] Universal reference와 Overloading
    C++/Effective Modern C++ 2022. 7. 28. 14:54

    이전 글(R-value reference와 Universal reference)에서 Universal reference(보편참조)가 무엇인지 살펴보았습니다.

    R-value, L-value객체 모두에 묶일 수 있는 범용성 높은 참조였습니다.

    그런데, 이런 범용성 때문에 문제가 발생할 수 있습니다.

    보편참조를 받는 함수를 오버로딩 할 때에 발생하는 문제점을 살펴보도록 하겠습니다.

     


     

    1. 예제

     

    더보기

    이름을 담은 문자열을 매개변수로 받고, 시간을 기록한 뒤 자료구조에 추가하는 함수를 가정해보도록 하겠습니다.

    그 형태는 아래와 같이 구현할 수 있을 것 입니다.

    std::multiset<std::string> names;
    
    void logAndAdd(const std::string& name) {
        auto now = std::chrono::system_clock::now();
        
        log(now, "logAndAdd"); // Loging function
        
        names.emplace(name);
    }

    위 함수를 아래와 같이 호출해보도록 하겠습니다.

    std::string uName("Name1");
    
    logAndAdd(uName); // L-value
    logAndAdd(std::string("Name2"); // R-value
    logAndAdd("Name3"); // R-value (string literal)

     위 세 호출은 각각 아래와 같이 처리됩니다.

    1. uName은 logAndAdd의 name에 전달되고, emplace를 통해 복사됩니다. L-value로 전달되므로 이 복사를 피할 수는 없습니다.
    2. Name2는 명시적으로 생성된 임시 std::string이 전달됩니다. 이 객체는 R-value지만, logAndAdd의 name이 L-value이므로 emplace를 통해 복사됩니다. 하지만, 이 복사를 이동으로 바꿀 수 있습니다.
    3. Name3의 경우도 logAndAdd의 name은 Name2의 명시적 생성과는 다른 암묵적 생성 객체입니다. Name2와 마찬가지로 emplace를 통해 복사되지만, 리터럴 문자열을 그대로 emplace에 전달할 경우 복사도, 이동도 필요 없습니다.

    각 경우를 살펴본 바, logAndAdd는 최적화의 여지가 존재합니다.

    함수를 보편참조를 이용하게 하고, 이전 글(std::move와 std::forward (2))에 따라 std::forward를 사용하도록 하겠습니다.

    template<typename T>
    void logAndAdd(T&&) {
        auto now = std::chrono::system_clock::now();
        
        log(now, "logAndAdd"); // Loging function
        
        names.emplace(std::forward<T>(name));
    }

    이렇게 수정할 경우 함수를 최적화 할 수 있습니다.

     

    2. 문제

     

    더보기

    만약 위 logAndAdd함수의 매개변수인 string을 직접 얻지 못 하는 클라이언트가 있다고 가정해보도록 하겠습니다.

    이 클라이언트는 이름에 대한 색인만 가지고 있고, logAndAdd함수에서 이 색인을 이용해서 이름을 가져와야 합니다.

    위와 같은 기능을 지원하기 위해 logAndAdd함수를 아래와 같이 오버로딩 할 수 있습니다.

    std::string nameFromIdx(int idx); // Index search function
    
    void logAndAdd(int idx) {
        auto now = std::chrono::system_clock::now();
        log(now, "logAndAdd");
        names.emplace(nameFromIdx(idx));
    }

    위 오버로딩이 추가된 상태에서, 아래와 같이 호출하도록 하겠습니다.

    std::string uName("Name1");
    
    logAndAdd(uName);
    logAndAdd(std::string("Name2");
    logAndAdd("Name3");
    
    logAndAdd(22);
    
    short nameIdx = 33;
    logAndAdd(nameIdx);

    위 3개의 호출은 기존의 함수 (보편참조를 받는 함수)를 호출합니다.

    22를 인자로 하는 함수도 오버로딩 된 함수를 호출합니다.

    하지만, short를 인자로 호출하는 5번째 호출은 오류가 발생합니다. 

     

    3. 보편참조와 오버로딩

     

    더보기

    위 문단의 short를 인자로 logAndAdd함수를 호출하는 상황을 조금 더 자세히 살펴보도록 하겠습니다.

    1. logAndAdd(T&&)의 경우, T를 short&로 연역되어 인수와 부합하는 형태가 됩니다.
    2. logAndAdd(int)의 경우, short를 int로 승격할 경우 인수와 부합하는 형태가 됩니다.

    오버로딩 해소 규칙에 따라 1의 경우가 2보다 우선되어, logAndAdd(T&&)가 호출됩니다.

    logAndAdd(T&&)의 emplace함수에 short가 전달되고, 이것은 다시 std::string의 생성자로 전달됩니다.

    하지만 이 때, std::string의 생성자 중 short를 받는 생성자가 없으므로 호출이 실패하게 됩니다.

     

    보편참조의 경우, 대부분의 형식 인수와 정확히 부합하는 형태가 됩니다. (대부분의 형식으로 연역이 가능하므로)

    따라서 위와 같이 의도하지 않은 오버로딩 함수가 호출되는 상황이 자주 발생할 수 있습니다.

     

    4. 보편참조와 생성자 오버로딩

     

    더보기

    위 문단에서 발생한 문제는 컴파일러가 의도하지 않은 오버로딩 함수를 호출하는 것 이었습니다.

    잘못된 함수를 호출하지 않도록 하기 위해, std::string을 한번 래핑한 Name 클래스를 만들어 보겠습니다.

    class Name {
    public:
        template<typename T>
        explicit Name(T&& n) : name(std::forward<T>(n)) {}
        
        explicit Name(int idx) : name(nameFromIdx(idx)) {}
        ...
    
    private:
        std::string name;
    };

    이와 같은 구현 또한 문제를 발생시킵니다.

    여전히 int 이외의 정수 형식 (short, long ... etc)을 인자로 할 경우 보편참조 생성자가 호출되며, 컴파일이 실패합니다.

    심지어, 추가적인 문제가 발생합니다.

    위 함수는 특수 멤버 함수들을 컴파일러가 자동으로 생성하여, 오버로딩 된 함수들이 개발자가 작성한 함수들보다 더 많이 생겨납니다.

    특수 멤버 함수에 관한 내용은 다른 글(Special member function)에 정리되어 있습니다.

    class Name {
    public:
        template<typename T>
        explicit Name(T&& n) : name(std::forward<T>(n)) {}
        
        explicit Name(int idx) : name(nameFromIdx(idx)) {}
        ...
        Name(const Name& rhs); // Copy constructor
        Name(Name&& rhs); // Move constructor
        ...
        
    private:
        std::string name;
    };

     따라서, 위 Name 클래스의 아래와 같은 사용은 컴파일 에러를 발생시킵니다.

    Name n("Name1");
    
    auto copyN(n);

    위 코드의 copyN은 복사 생성자의 호출을 의도하고 작성한 코드입니다.

    하지만 이 코드는 Name(T&&)생성자를 호출하게 되고, 위 문단의 경우와 같이 Name을 받는 std::string 생성자가 존재하지 않아 컴파일에 실패하는 것 입니다.

    조금 더 구체적으로, copyN의 인스턴스화가 완료된 Name 클래스는 아래와 같습니다.

    class Name {
    public:
        explicit Name(Name&& n) : name(std::forward<Name&>(n)) {}
        
        explicit Name(int idx) : name(nameFromIdx(idx)) {}
        
        Name(const Name& rhs); // Copy constructor
        ...
    };

    이 때 복사 생성자를 호출하려 할 경우 n에 const를 추가해야 하지만, 아무것도 추가하지 않아도 Name&&와 형식이 부합됩니다. 따라서, 오버로딩 해소 규칙에 따라 템플릿 생성자가 호출됩니다.

    n을 const로 선언할 경우에는 상황이 달라집니다.

    const Name n("Name1");
    
    auto copyN(n);
    
    ...
    
    class Name {
    public:
        explicit Name(const Name& n) : name(std::forward<Name&>(n)) {}
        
        explicit Name(int idx) : name(nameFromIdx(idx)) {}
        
        Name(const Name& rhs); // Copy constructor
        ...
    };

    이 경우 템플릿 생성자가 복사 생성자의 서명과 동일해집니다.

    하지만 오버로딩 해소 규칙 중 템플릿과 일반 함수가 동일하게 부합할 경우, 일반 함수를 우선하는 규칙이 있기 때문에 위 경우에는 일반 함수가 호출됩니다.

     

    5. 보편참조와 상속

     

    더보기

    위 문단의 상황에 상속이 추가될 경우 문제가 더욱 복잡해집니다.

    예제를 살펴보도록 하겠습니다.

    class NameExtend : public Name {
    public:
        NameExtend(const NameExtend& rhs) : Name(rhs) { ... } // Copy constructor
        NameExtend(NameExtend&& rhs) : Name(std::move(rhs)) { ... } // Move constructor
        ...
    };

    Name을 상속받는 NameExtend클래스의 복사, 이동 생성자는 위와 같이 작성할 수 있습니다.

    하지만 각각의 복사, 이동 생성자가 호출하는 함수는 상위 클래스의 복사, 이동 생성자가 아닙니다.

    두 생성자 모두 템플릿 생성자를 호출하게 됩니다. 

    Name의 템플릿 생성자는 위 문단의 경우와 동일하게 std::string 생성자가 존재하지 않기 때문에 컴파일 되지 않습니다.

     


     

    이번 글에서는 보편참조 함수를 오버로딩 할 경우 발생하는 문제를 살펴보았습니다.

    보편참조 함수에 대한 오버로딩은 가급적이면 피하는 것이 좋습니다.

    하지만, 보편참조를 사용하면서 특정 매개변수에 대한 특별한 처리가 필요한 경우가 있을 수 있습니다.

    다음 글에서, 이러한 특별한 경우에 사용할 수 있는 기법을 알아보도록 하겠습니다.

     

    감사합니다.

    댓글

Designed by Tistory.