※ Strategy Pattern

전략패턴은 객체들의 행위를 각 전략 클래스에 생성, 유사행위를 캡슐화하는 인터페이스를 정의

객체의 행위를 동적으로 바꾸고 싶다면 전략만 바꿔줌으로 행위를 유연하게 확장하는 방법

클라이언트와 독립적으로 구현되기에 새로운 알고리즘 추가,변경이 쉬워진다.

[구조 예시]

 

#include <iostream>
using namespace std;

class SortBehavior {
public:
    virtual void sort() const = 0;
};
class Merge: public SortBehavior {
public:
    virtual void sort() const { cout << "Merge sort()" << endl; }
};
class Quick: public SortBehavior {
public:
    virtual void sort() const { cout << "Quick sort()" << endl; }
};
class Heap: public SortBehavior {
public:
    virtual void sort() const { cout << "Heap sort()" << endl; }
};

class SearchBehavior {
public:
    virtual void search() const = 0;
};
class Sequential: public SearchBehavior {
public:
    virtual void search() const { cout << "Sequential search()\n"; }
};
class BinaryTree: public SearchBehavior {
public:
    virtual void search() const { cout << "BinaryTree search()\n"; }
};
class HashTable: public SearchBehavior {
public:
    virtual void search() const { cout << "HashTable search()\n"; }
};

// Context
class Collection {
private:
    SortBehavior *m_sort;
    SearchBehavior *m_search;
public:
    Collection(){}
    void set_sort(SortBehavior *s) { m_sort = s; }
    void set_search(SearchBehavior *s) { m_search = s; }
    void sort() const { m_sort->sort(); }
    void search() const { m_search->search(); }
};


int main(int argc, char *argv[])
{
    Merge merge;
    Quick quick;
    Heap heap;

    Sequential sequential;
    BinaryTree binaryTree;
    HashTable hashTable;

    Collection colA;
    colA.set_sort(&merge);
    colA.sort();

    Collection colB;
    colB.set_search(&binaryTree);
    colB.search();

}

 

 

 

 

 

 

 

 

※ Observer Pattern

객체의 변화를 관찰하는 observer들의 목록을 객체에 등록, 변화가 있을 때 함수를 이용해 관찰대상자가 직접 observer에게 통지해 그 객체에 의존성을 가진 다른 객체가자동으로 업데이트 하는 방식

[구조 예시]

● Generator: 관찰 대상자로 현재 관찰 대상자에 붙어있는 Observer들을 관리할뿐만 아니라 현재 관찰 대상자의 상태 정보를 얻기 위한 함수를 제공
           상태 변화시 등록되어 있는 모든 관찰자들에게 상태 변화를 통지해주는 함수를 제공합니다.

● StringGenerator: Generator를 상속받는 실제 상태 정보를 가지고 있는 객체.
                  상태 변화가 발생하면 상태 변화를 통지해주는 함수를 호출.

● Observer: 관찰자들이 가져야 할 공통인터페이스를 정의.

● StringObserver: 관찰 대상자의 상태 정보를 가져와 자신의 상태와 동기화.
                 이 객체는 관찰 대상자의 string형을 모니터에 출력해주는 객체입니다.

● StringCountObsever: 마찬가지로 관찰 대상자의 상태 정보를 가져와 자신의 상태와 동기화 합니다. 
                      이 객체는 관찰 대상자인 string형 문자열의 개수를 화면에 출력하는 객체

 

#include <iostream>
#include <list>
#include <string>
using namespace std;

class IObserver {
public:
    virtual void Update(const string &message_from_subject) = 0;
    virtual ~IObserver(){ };
};

class ISubject {
public:
    virtual void Attach(IObserver *observer) = 0;
    virtual void Detach(IObserver *observer) = 0;
    virtual void Notify() = 0;
    virtual ~ISubject(){};
};

/* Subject는 일부 중요한 state를 소유, state가 변경되면 observer에게 알림*/
class Subject : public ISubject {
public:
    /* subscription 관리 함수 */
    void Attach(IObserver *observer) override { list_observer_.push_back(observer); }
    void Detach(IObserver *observer) override { list_observer_.remove(observer); }
    void Notify() override {
        list<IObserver *>::iterator iterator = list_observer_.begin();
        HowManyObserver();
        while (iterator != list_observer_.end()) {
            (*iterator)->Update(message_);
            ++iterator;
        }
    }
    void CreateMessage(string message = "Empty") {
        this->message_ = message;
        Notify();
    }

    void HowManyObserver() { cout << "There are " << list_observer_.size() << " observers in the list" << endl; }
    
    /*
     * 일반적으로 subscription logic은 Subject가 실제로 수행할 수 있는 작업의 일부이다.
     * Subject는 일반적으로 중요한 일이 발생할 때마다 통지 방법을 작동시키는 중요한 business logic를 갖고 있다.
     */
    void SomeBusinessLogic() {
        this->message_ = "change message message";
        Notify();
        cout << "I'm about to do some thing important\n";
    }

    virtual ~Subject() { cout << "Goodbye, I was the Subject" << endl; }

private:
    list<IObserver *> list_observer_;
    string message_;
};

class Observer : public IObserver {
public:
    Observer(Subject &subject) : subject_(subject) {
        this->subject_.Attach(this); // this는 observer
        cout << "Hi, I'm the Observer \"" << ++Observer::static_number_ << "\"" << endl;
        this->number_ = Observer::static_number_;
    }
    virtual ~Observer() {
        cout << "Goodbye, I was the Observer \"" << this->number_ << "\"" << endl;
    }

    void Update(const string &message_from_subject) override {
        message_from_subject_ = message_from_subject;
        PrintInfo();
    }
    void RemoveMeFromTheList() {
        subject_.Detach(this); // this는 observer
        cout << "Observer \"" << number_ << "\" removed from the list" << endl;
    }
    void PrintInfo() {
        cout << "Observer \"" << this->number_ << "\": a new message is available --> " << this->message_from_subject_ << endl;
    }

private:
    std::string message_from_subject_;
    Subject &subject_;
    static int static_number_;
    int number_;
};

int Observer::static_number_ = 0;   // static멤버변수 초기화 방법

void ClientCode() {
    Subject *subject = new Subject;
    Observer *observer1 = new Observer(*subject);
    Observer *observer2 = new Observer(*subject);
    Observer *observer3 = new Observer(*subject);
    Observer *observer4;
    Observer *observer5;

    subject->CreateMessage("Hello World! :D");
    observer3->RemoveMeFromTheList();

    subject->CreateMessage("The weather is hot today! :p");
    observer4 = new Observer(*subject);

    observer2->RemoveMeFromTheList();
    observer5 = new Observer(*subject);

    subject->CreateMessage("My new car is great! ;)");
    observer5->RemoveMeFromTheList();

    observer4->RemoveMeFromTheList();
    observer1->RemoveMeFromTheList();

    delete observer5;
    delete observer4;
    delete observer3;
    delete observer2;
    delete observer1;
    delete subject;
}

int main() {
    ClientCode();
}

 

 

 

 

 

 

 

 

※ Adapter Pattern

변환기처럼 서로 다른 두 인터페이스 사이 통신을 가능하게 해주는 디자인 패턴이다.프로그램에서 한 클래스의 인터페이스를 클라이언트로 사용하고자 하는 인터페이스로 변환 시 사용

또한 어댑터 패턴은 다중상속을 사용해 구현할 수도 있다.

[구조 예시]

 

 

 

#include <iostream>
#include <algorithm>
#include <string>
using namespace std;

/* Target: client code에서 사용되는 domain-specific interface를 정의한다. */
class Target {
public:
    virtual std::string Request() const { return "Target: The default target's behavior."; }
    virtual ~Target() = default;
};

/* Adaptee(기존객체)는 유용한 동작이 포함되어 있지만 interface가 기존client code와 호환되지 않는다.
 * 따라서 client code가 Adaptee를 사용하려면 적응할 필요가 있다.
 */
class Adaptee {
public:
    string SpecificRequest() const { return ".eetpadA eht fo roivaheb laicepS"; }
};

/* Adapter는 Adaptee의 interface가 Target's interface와 호환되게 한다. */
class Adapter : public Target {
private:
    Adaptee *adaptee_;
public:
    Adapter() { }
    Adapter(Adaptee *adaptee) : adaptee_(adaptee) {}

    string Request() const override {
        string to_reverse = this->adaptee_->SpecificRequest();
        reverse(to_reverse.begin(), to_reverse.end());
        return "Adapter: (TRANSLATED) " + to_reverse;
    }
};

/* client code는 Target interface를 따르는 모든 클래스를 지원한다. */
void ClientCode(const Target *target) { cout << target->Request(); }

int main() {
    cout << "Client: I can work just fine with the Target objects:\n";

    Target *target = new Target;
    ClientCode(target);
    cout << endl << endl;

    Adaptee *adaptee = new Adaptee;
    cout << "Client: The Adaptee class has a weird interface. See, I don't understand it: " << endl;
    cout << "Adaptee: " << adaptee->SpecificRequest();
    cout << endl << endl;
    cout << "Client: But I can work with it via the Adapter: " << endl;

    Adapter *adapter = new Adapter;
    ClientCode(adapter);
    cout << endl;

    delete target;
    delete adaptee;
    delete adapter;
}

 

 

 

 

 

[다중상속으로 구현한 코드]

#include <iostream>
#include <algorithm>
#include <string>
using namespace std;

class Target {
public:
    virtual string Request() const { return "Target: The default target's behavior."; }
    virtual ~Target() = default;
};

class Adaptee {
public:
    string SpecificRequest() const { return ".eetpadA eht fo roivaheb laicepS"; }
};

class Adapter : public Target, public Adaptee {
public:
    Adapter() { }
    string Request() const override {
        string to_reverse = SpecificRequest();
        reverse(to_reverse.begin(), to_reverse.end());
        return "Adapter: (TRANSLATED) " + to_reverse;
    }
};

void ClientCode(const Target *target) { cout << target->Request(); }

int main() {
    cout << "Client: I can work just fine with the Target objects: " << endl;
    
    Target *target = new Target;
    ClientCode(target);
    cout << endl << endl;
    
    Adaptee *adaptee = new Adaptee;
    cout << "Client: The Adaptee class has a weird interface. See, I don't understand it: " << endl;
    cout << "Adaptee: " << adaptee->SpecificRequest();
    cout << endl << endl;
    cout << "Client: But I can work with it via the Adapter: " << endl;
    
    Adapter *adapter = new Adapter;
    ClientCode(adapter);
    cout << endl;

    delete target;
    delete adaptee;
    delete adapter;
}

 

※ Design Pattern이란?

[Buschmann, et al. 1996]_ 최고의 캡슐화의 한 방법으로 매우 효율적으로 프로그래머들이 문제를 해결할 수 있을 것이다.

안정성, 확장성 등에도 효율적이며 패턴이 반복되는 설계문제를 해결하도록 하며 대표적으로 다음과 같은 예시들이 있다.

 

이렇게 많은 디자인 패턴 종류에서 유명한 3가지인 싱글톤, 팩토리 메소드, 브릿지 패턴을 이번에 정리할 것이다.

 

 

※ Singleton

하나의 (전역)인스턴스만 생성하여 사용하는 디자인 패턴.

인스턴스가 필요할 때, 기존의 인스턴스를 활용하는 것.

한번의 new를 통한 객체생성으로 메모리 낭비를 방지가능!

딱 하나의 독자적인 클래스생성을 진행하며 그 클래스의 객체가 복사되면 안된다.

[구조]

class Singleton {
private:
    // 생성자는 private으로 막는다.
    // 따라서 외부에서 new를 이용한 객체생성이 불가능하다.
    Singleton();

    Singleton(const Singleton& ref) { }

    Singleton& operator=(const Singleton& ref) { }

    ~Singleton() { }

    // 객체 하나를 담을 수 있는 포인터 변수를 선언
    // 이때, static으로 선언해서 단 하나만 존재할 수 있게 한다.
    static Singleton *single;

public:
    // single을 가져오거나 해제하는 멤버함수 선언
    // static변수에 접근하고 외부에서 쓸 수 있어야 해서 publc으로 지정
    static Singleton *getInstance();
    static void DestroySingle();
};

Singleton *Singleton::single = nullptr;     // static멤버변수이기에 클래스 외부에서 초기화
Singleton::Singleton() { }

Singleton *Singleton::getInstance() {   
    if (!single)        // single이 nullptr일 때, 
        single = new Singleton;   // 새로 생성해 객체를 초기화
    return single;      // 앞으로 호출될 때마다 이미 생성된 객체를 return
}

void Singleton::DestroySingle() {   // getInstance() 함수와 유사
    if (!single)
        return;
    delete single;
    single = nullptr;
}

 

 

 

 

 

※ Factory Method

객체생성이 복잡하거나 어렵다면, 이를 대행하는 함수를 두는 설계방식.객체를 생성하지만 생성자를 호출하지 않고 대행함수를 통해 간접적으로 객체를 생성한다.

팩토리 메서드 패턴은 복잡도가 낮고 높은 수준의 유연성을 제공해야할 때, 매우 유용하다.

즉, 생성자 기능을 대신하는 메소드를 별도로 정의하는 방식이다.

[구조 예시]

#include <iostream>
#include <string>
using namespace std;


class Product {  // 인터페이스(interface) 선언
public:
    virtual string Operation() const = 0;
    virtual ~Product() { }
};

class ConcreteProduct1 : public Product {   // 인터페이스 상속
public:
    string Operation() const override {
        return "{Result of the ConcreteProduct1}";
    }
};
class ConcreteProduct2 : public Product {   // 인터페이스 상속
public:
    string Operation() const override {
        return "{Result of the ConcreteProduct2}";
    }
};

/* Product 인터페이스의 객체를 return하는 factory method를 선언하는 Creator 클래스
 * Creator클래스의 하위 클래스는 factory method를 상속받음 */

class Creator {     // 인터페이스 클래스는 선언만 진행 (구현X)
public:
    virtual Product *FactoryMethod() const = 0;
    virtual ~Creator(){};

    /* factory method에서 반환된 product 객체에 의존한다. */
    string SomeOperation() const {

        // Product객체생성을 위한 factory method 호출.
        Product *product = this->FactoryMethod();

        string result = "Creator: 동일한 creator의 코드가 " + product->Operation() + "과 작동중";
        delete product;
        return result;
    }
};

/* product type변환을 위해 factory method를 재정의하는  Concrete Creator 클래스들 */
class ConcreteCreator1 : public Creator {
public:
    Product *FactoryMethod() const override {
        return new ConcreteProduct1();
    }
};
class ConcreteCreator2 : public Creator {
public:
    Product *FactoryMethod() const override {
        return new ConcreteProduct2();
    }
};

/* ClientCode 함수는 ConcreteCreator 객체와 함께 작동
 * 기본 인터페이스로 어떤 creator 하위클래스에도 전달 가능 */
void ClientCode(const Creator& creator) {
    cout << "Client: I'm not aware of the creator's class, but it still works.\n" << creator.SomeOperation() << endl;
}

int main() {
    cout << "App: Launched with the ConcreteCreator1.\n";
    Creator *creator = new ConcreteCreator1();
    ClientCode(*creator);
    
    cout << endl;
    
    cout << "App: Launched with the ConcreteCreator2.\n";
    Creator *creator2 = new ConcreteCreator2();
    ClientCode(*creator2);

    delete creator;
    delete creator2;
    return 0;
}

 

 

 

 

 

 

 

 

 

※ Bridge pattern

구현부에서 추상층을 분리, 각각 독립적으로 변형과 확장이 가능하게 하는 패턴  java에선 super키워드도 사용

따라서 두 계층 모두 추상화된 상위 타입을 갖고 의존성은 상위 타입간에만 이뤄진다.

인터페이스와 구현방식이 완전 결합되는 것을 피할 때 사용

하위 클래스 구조가 서로 다른 형태이길 원할 때 사용

 

과거 C++개발자들은 컴파일 단축을 위해 Pimpl이라는 독특한 관례를 사용했다. (Pointer to Implement)

Pimpl은 말그대로 구현부를 포인터로 참조하는 것이다.

장점 1: 클래스의 private, protected멤버가 헤더로 노출되기에 불필요한 노출을 막을 수 있다.

장점 2: 숨겨진 구현클래스에 대한 수정이 바이너리 호환성에 영향X

장점 3: 헤더파일에 구현에 필요한 다른 헤더를 포함하지 않아도 된다.

 

컴포넌트 간 다양한 조합이 가능할 때 효과적이며 실제 구현을 모두 인터페이스에 위임한다.

[구조 예시]

 

 

Abstraction: 기능 계층최상위 클래스.

구현부분에 해당하는 클래스를 객체를 이용구현부분의 함수를 호출

Refind Abstraction: 기능 계층에서 새로운 부분을 확장한 클래스

 

Implementation: 추상클래스의 기능구현하기 위한 인터페이스 정의

Concrete Implementor: 실제 기능구현하는 것

 

#include <iostream>
#include <string>
using namespace std;

/******************************<인터페이스 구현>******************************/
class Implementation {
public:
    virtual string OperationImplementation() const = 0;     // 순수가상함수
    virtual ~Implementation() { }
};
class ConcreteImplementationA : public Implementation {
public:
    string OperationImplementation() const override  {
        return "ConcreteImplementationA: Here's the result on the platform A.\n";
    }
};
class ConcreteImplementationB : public Implementation {
public:
    string OperationImplementation() const override {
        return "ConcreteImplementationB: Here's the result on the platform B.\n";
    }
};

/******************************<추상클래스 구현>******************************/
class Abstraction {
protected:
    // 인터페이스 클래스에 대한 포인터참조로 브릿지 패턴을 잘 나타내고 있음을 알 수 있다.
    Implementation *implementation_;    
public:
    Abstraction(Implementation *implementation) : implementation_(implementation) { }
    virtual string Operation() const {
        return "Abstraction: Base operation with:\n" + this->implementation_->OperationImplementation();
    }
    virtual ~Abstraction() { }
};
class ExtendedAbstraction : public Abstraction {
public:
    ExtendedAbstraction(Implementation* implementation) : Abstraction(implementation) { }
    string Operation() const override {
        return "ExtendedAbstraction: Extended operation with:\n" + this->implementation_->OperationImplementation();
    }
};

void ClientCode(const Abstraction& abstraction) {
    cout << abstraction.Operation();
}

int main() {
    Implementation *implementation = new ConcreteImplementationA;
    Abstraction *abstraction = new Abstraction(implementation);
    ClientCode(*abstraction);

    cout << endl;

    delete implementation;
    delete abstraction;

    implementation = new ConcreteImplementationB;
    abstraction = new ExtendedAbstraction(implementation);
    ClientCode(*abstraction);

    delete implementation;
    delete abstraction;
}

출처: https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=kyung778&logNo=60154874584

 

 

 

다음 Java 로직을 C++로 구현해 보자.

 

[Java]

// strategy.java
public interface Strategy {
        public int doOperation(int num1, int num2);
}

// OperationAdd.java
public class OperationAdd implements Strategy{
@Override
    public int doOperation(int num1, int num2) {
        return num1 + num2;
    }
}
// OperationSubstract.java
public class OperationSubstract implements Strategy{
    @Override
    public int doOperation(int num1, int num2) {
        return num1 - num2;
    }
}
// OperationMultiply.java
public class OperationMultiply implements Strategy{
@Override
    public int doOperation(int num1, int num2) {
        return num1 * num2;
    }
}
// Context.java
public class Context {
    private Strategy strategy;
    public Context(Strategy strategy){
            this.strategy = strategy;
        }
    public int executeStrategy(int num1, int num2){
            return strategy.doOperation(num1, num2);
        }
}
// StrategyPatternDemo.java
public class StrategyPatternDemo {
    public static void main(String[] args) {
            Context context = new Context(new OperationAdd());
            System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
            context = new Context(new OperationSubstract());
            System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
            context = new Context(new OperationMultiply());
            System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
        }
}

 

 

 

[C++]

#include <iostream>
using namespace std;

class Strategy{
public:
    virtual int doOperation (int num1, int num2) = 0;
};

class OperationAdd : public Strategy{
public:
    int doOperation(int num1, int num2) override{
        return num1 + num2;
    }
};
class OperationSubstract : public Strategy{
public:
    int doOperation(int num1, int num2) override{
        return num1 - num2;
    }
};
class OperationMultiply : public Strategy{
public:
    int doOperation(int num1, int num2) override{
        return num1 * num2;
    }
};

class Context {
private:
    Strategy *strategy;
public:
    Context(Strategy *strategy){ this->strategy = strategy; }
    int executeStrategy(int num1, int num2) { return strategy->doOperation(num1, num2); }
};

int main(){
    Context *context = new Context(new OperationAdd);
    cout << "10 + 5 = " << context->executeStrategy(10, 5) << endl;

    context = new Context(new OperationSubstract());
    cout << "10 - 5 = " << context->executeStrategy(10, 5) << endl;

    context = new Context(new OperationMultiply());
    cout << "10 * 5 = " << context->executeStrategy(10, 5) << endl;
}

 

핵심 포인트!

1. private에 Strategy 클래스의 객체를 생성할 수 있다.

2. 생성자의 매개변수로 객체를 생성할 수 있다.

class Context {
private:
    Strategy *strategy;
public:
    Context(Strategy *strategy){ this->strategy = strategy; }
    int executeStrategy(int num1, int num2) { return strategy->doOperation(num1, num2); }
};

 

§ [2.6 조언] _  어떻게 C++로 멋진 프로그램을 짤 수 있을까?

- 가능한 경우, 정적으로 타입체크되도록 한다.

- 정보는 지역적으로 보관한다. (전역변수와 포인터의 사용을 최소화)

- 지나치게 추상화하지 않는다.

- C++의 세부사항까지 전부 알 필요는 없다.

 

 

§ [3.5 조언] _ 추상화 메커니즘

- 무방비의 new와 delete연산은 피한다.

- 인터페이스와 구현의 완벽한 분리가 필요하다면, 추상클래스를 인터페이스로 활용한다.

- 클래스 계층구조 설계시, 구현상속과 인터페이스 상속을 구분한다.

- 객체의 복사, 이동, 소멸을 통제해야 한다.

- 컨테이너는 값으로 반환한다. (효율성이 중요하다면, move를 이용)

- 함수 템플릿으로 일반적인 알고리즘을 표현한다.

- 람다를 비롯한 함수객체를 이용해 정책과 작동을 표현한다.

- 타입, 템플릿 별칭을 이용해 유사한 기능은 동일한 이름을 부여한다.

 

 

 

§ [4.6 조언] _ 컨테이너와 알고리즘

- 선택가능하다면, 다른 라이브러리보다는 표준라이브러리(std namespace에 정의된)를 우선사용한다.

- 표준라이브러리가 만능이라 생각하지 말자.

- C 스타일 문자열(char*) 보다는 string을 우선사용한다.

- arr[T] 보다는 vector<T>, map<K,V>, unordered_map를 우선 사용한다.

- vector를 기본 컨테이너로 활용한다.

- push_back()  ||  back_inserter 를 통해 컨테이너에 원소를 추가한다.

- 배열에 realloc을 사용하지 않고, vector에 push_back()을 사용한다.

- 일반적인 예외는 main()문에서 잡는다(catch).

- 표준 알고리즘에 대해 직접 만들기 보단 표준알고리즘을 우선사용한다.

- 반복자가 장황해지면 컨테이너 알고리즘을 정의한다.

- 완전한 컨테이너 범위기반 for문을 사용한다.

 

 

 

 

§ [5.7 조언] _ 병행성과 유틸리티

- unique_ptr을 이용해 다형성타입 객체를 참조한다.

- shared_ptr을 이용해 공유객체를 참조한다.

- 공유데이터의 사용은 최소화한다.

- 심사숙고없이 효율성이라는 이유만으로 통신을 위해 공유데이터를 선택하지 말 것.

- 간단한 패턴매칭을 위해서는 정규표현식을 사용한다.

- 수치타입의 속성은 numeric_limits로 접근할 수 있다.

- 수치계산처리를 시도할 때는 라이브러리를 활용하자.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

※ 형식연역 (type deduction)

형식연역이 일어나는 방식을 확실히 이해하지 않는다면 Modern C++에서 효과적인 프로그래밍은 불가능하다.

1장에서는 이런 형식연역의 작동방식과 형식연역에 기초한 auto와 decltype의 작동방식에 대해 알아본다.

필요성 : 템플릿 형식 연역 규칙들이 auto의 문액에 적용 될 때 덜 직관적이며
auto를 잘 활용하려면 템플릿 형식 연역을 이해하고 있어야 한다.

 

 

 

 

 

User가 System의 작동방식을 알지 못해도 System의 일에 만족한다면, 잘 설계되었다라고도 할 수 있다.

 

그런 프로그래머라면 좋은 소식과 나쁜 소식이 있다.

- Positive: Modern C++은 아주 강한 기능 중 하나인 auto가 template 형식영역을 기반으로 작동한다는 것이다.

- Negative: template형식 연역규칙들이 auto의 문맥에 적용될 때 template에 비해 덜 직관적인 경우가 있다.

 

∴ 따라서 auto를 잘 활용하려면 auto가 기초하는 template 형식연역의 면모를 제대로 이해해야한다.

 

 

 

※ 함수 template

template<typename T>
void f(ParamType param);

// 어떤 표현식으로 f를 호출
f(expr);

컴파일 도중 expr을 이용해 2가지의 형식을 연역한다.

1. T에 대한 형식

2. ParamType에 대한 형식

 

이 두 형식이 다른 경우가 많은데 이는 ParamType에 흔히 const 같은 참조한정사 수식어가 붙기 때문이다.

template<typename T>
void f(const T& param); //ParamType은 const T&

int x = 0;

f(x); // int로 f를 호출

이 경우, T는 int로 연역되지만 ParamType은 cosnt int& 로 연역된다. (x는 int이고 T는 int로 연역된다.)

 

T에 대해 연역된 형식이 함수에 전달된 인수의 형식과 같다고 생각했다면 그것이 항상 그러지는 않는다.

=> T에 대해 연역된 형식은 expr의 형식에 의존할 뿐만 아니라 ParamType의 형태에도 의존한다.

그 형태에 따라 총 3가지 경우로 나뉜다.

1. ParamType이 포인터, 참조형식이지만 보편참조는 아닌 경우.
2. ParamType이 보편참조인 경우
3. ParamType이 포인터, 참조 모두 아닌 경우

따라서 3가지 형식 연역 시나리오에 대해 살펴봐야 한다.

 

다음과 같은 일반적인 형태의 템플릿과 그 호출에 기초해 3가지 시나리오를 생각해보자.

template<typename T>
void f(ParamType param);

f(expr);  // expr로부터 T와 ParamType을 연역

 

 

Case 1. ParamType이 포인터, 참조형식 이지만 보편참조는 아님.

1. 만약 expr이 참조형식이면 참조부분을 무시한다.

2. 그 후 expr의 형식을 ParamType에 대해 pattern-matching방식으로 대응시켜 T의 형식을 결정한다.

template<typename T>
void f(T& param);  // param은 참조형식

int x = 27;        // x는 int
const int cx = x;  // cx는 const int
const int& rx = x; // rx는 const int인 x에 대한 참조

/* 여러 호출에서 param과 T에 대해 연역된 형식 */
f(x);  // T는 int, param의 형식은 int&
f(cx); // T는 const int, param의 형식은 const int&
f(rx); // T는 cosnt int, param의 형식은 const int&

위 f(cx), f(rx)의 param은 const값이고, 이는 그 객체가 수정되지 않을 것이라 기대한다.

즉, 해당 매개변수가 const에 대한 참조일 것이라 기대한다. (T& 매개변수를 받는 템플릿에 const객체를 전달해도 안전한 이유)

 

f(rx)에서 비록 rx의 형식이 참조이지만 T는 비참조로 연역된 것에 주목하면 이는 형식연역과정에서 rx의 참조성이 무시되기 때문임을 알 수 있다.

만약 f의 매개변수 형식을 T&에서 const T&로 바꾸게 된다면 상황이 달라지지만 큰 변화는 없다.
cx와 rx의 const성은 유지되며 단, param이 const에 대한 참조로 간주되므로  
const가 T의 일부로 연역될 필요는 없다.
template<typename T>
void f(const T& param);  // param이 cosnt에 대한 참조

int x = 27;        // x는 int
const int cx = x;  // cx는 const int
const int& rx = x; // rx는 const int인 x에 대한 참조

/* 여러 호출에서 param과 T에 대해 연역된 형식 */
f(x);  // T는 int, param의 형식은 const int&
f(cx); // T는 int, param의 형식은 const int&
f(rx); // T는 int, param의 형식은 const int&

이전처럼 rx의 참조성은 형식연역과정에서 무시된다.

 

이는 아래처럼 param을 참조가 아닌 포인터, const를 가리키는 포인터라도 형식연역은 본직적으로 같은 방식을 취한다.

template<typename T>
void f(T* param);  // param이 cosnt에 대한 참조

int x = 27;        // x는 int
const int *px = x; // px는 const int로 x를 가리키는 포인터

/* 여러 호출에서 param과 T에 대해 연역된 형식 */
f(&x);  // T는 int, param의 형식은 int*
f(px); // T는 const int, param의 형식은 const int*

 

여기까지는 너무 쉬워서 하품이 날 지경이라고 한다.(물론 저자가 그런거지 난 아니다. 어려워 죽을거 같다.)

 

 

 

 

 

 

Case 2. ParamType이 보편참조임.

템플릿이 보편참조매개변수를 받는 경우, 상황이 불투명해진다.

그런 매개변수 선언은 rvalue 참조와 같은 모습이다. (보편참조의 선언형식은 T&&이다.)

l-value가 전달되면 r-value참조와는 다른 방식으로 행동한다.

 

- 만약 expr이 l-value면, T와 ParamType 둘 다 l-value 참조로 연역된다. (비정상적 상황)

  i) 템플릿 형식 연역게서 T가 참조형식으로 연역되는 경우, 이것이 유일

  ii) ParamType의 선언구문은 r-value 참조와 같지만 연역된 형식은 l-value참조이다.

- 만약 expr이 r-value면, '정상적인' 규칙들이 적용된다. (Case 1의 규칙들.)

template<typename T>
void f(T&& param);  // 이번에는 param이 보편 참조

int x = 27;         // x는 int
const int cx = x;   // cx는 const int
const int& rx = x;  // rx는 const int인 x에 대한 참조

f(x);  // x는 l-value, 따라서 T는 int&, param 형식도 int&
f(cx); // cx는 l-value, 따라서 T는 const int&, param 형식도 const int&
f(rx); // rx는 l-value, T는 const int&, param 형식도 const int&
f(27); // 27은 r-value, 따라서 T는 int, param 형식은 int&&

나중에 item 24에서 설명될 것이고, 지금은 보편참조매개변수에 관한 형식연역 규칙들이

l-value참조나 r-value참조 매개변수들에 대한 규칙들과는 다르다는 점만 기억하면 된다.

특히 보편참조가 관여하는 경우, l-value와 r-value에 대해 서로 다른 연역규칙이 적용된다. (보편참조가 아니면 발생X)

 

 

 

 

 

 

 

Case 3. ParamType이 포인터도 아니고 참조도 아님

ParamType이 포인터도 참조도 아닌 경우, 인수가 함수에 값으로 전달되는 pass-by-value인 상황이다.

template<typename T>
void f(T param);     // param이 값으로 전달된다.

따라서 param 주어진 인수의 복사본 (즉, 완전히 새로운 객체)이다.

param이 새로운 객체라는 사실 때문에 expr에서 T가 연역되는 과정에서 다음과 같은 규칙들이 적용된다.

1. 이전처럼, 만약 expr의 형식이 참조라면, 참조부분은 무시된다.
2. expr의 참조성을 무시한 후 만약 expr이 const라 해도 그 const 역시 무시한다.

만약 volatile객체라도 그것도 무시한다.   (volatile객체는 흔하지 않기에 (장치구동기 구현시 사용) item 40 참고)

 

다음은 이 규칙들이 적용되는 예이다.

template<typename T>
void f(T&& param);  // 이번에는 param이 보편 참조

int x = 27;         // x는 int
const int cx = x;   // cx는 const int
const int& rx = x;  // rx는 const int인 x에 대한 참조

f(x);  // T와 param의 형식 둘 다 int
f(cx); // 여전히 T와 param의 형식 둘 다 int
f(rx); // 여전히 T와 param의 형식 둘 다 int

이때, cx와 rx는 const값이지만, param은 const가 아님을 주목하자.

param은 cx, rx의 복사본이므로 (param은 cx, rx와 달리 완전히 독립적인 객체이므로) 당연한 결과이다.

cx, rx가 수정될 수 없다는 점(const)은 param의 수정가능 여부와는 무관하기 때문이다.

param의 형식을 연역하는 과정에서 expr의 const성이 무시되는 이유가 바로 이것이다.

expr을 수정할 수 없다해서 그 복사본까지 수정할 수 없는 것은 아니기 때문이다.

 

여기서 명심할 점은, const가 값 전달 매개변수에 대해서만 무시된다는 점이다.

즉, const에 대한 참조나 포인터 매개변수의 경우, 형식연역과정에서 expr의 const성이 보존된다.

 

 

그러나 expr이 cosnt객체를 가리키는 const포인터이고 param에 값으로 전달되는 경우는 어떨까?

template<typename T>
void f(T param);                    // 인수는 param에 여전히 값으로 전달됨

const char* const ptr = "V2LLAIN";  // ptr은 const 객체를 가리키는 const 포인터

f(ptr);                             // const char* const 형식의 인수를 전달

포인터 선언 오른쪽 const로 ptr 자체가 const가 된다. (const char* const ptr)

즉, ptr을 다른 장소를 가리키도록 변경불가능! (nullptr도 배정할 수 없다.)

 

포인터 선언 왼쪽 const가 가리키는 것은 문자열이 const인 것을 의미한다. (const char* const ptr)

 

ptr을 f에 전달하면 그 포인터를 구성하는 bit들이 param에 복사되며 포인터자체(ptr)는 값으로 전달된다.

형식연역과정에서 ptr의 const성은 무시되며 따라서 param에 연역되는 형식은 const char*이다.

즉, 형식연역과정에서 ptr이 가리키는 것의 const성은 보존되나

ptr 자체의 const성은 ptr을 복사해 새 포인터 param을 생성하는 도중 사라진다!

 

 

 

※ 템플릿 연역과정에서 배열이나 함수이름에 해당하는 인수는 포인터로 붕괴(decay)한다.

(단, 그런 인수가 참조를 초기화하는데 사용되면, 그 경우에는 포인터로 붕괴하지 않는다.)

 

§ 배열과 포인터를 구분하지 않고 사용가능하지만, 배열형식은 포인터 형식과 다르다는 사실!

- 배열과 포인터를 맞바꿔 쓸 수 있는 것처럼 보이는 환상의 주된 원인은 많은 문맥에서 배열이 배열의 첫 원소를 가리키는 포인터로 붕괴한다(decay)는 점이다.

이런 붕괴때문에 아래와 같은 코드는 오류가 없이 컴파일 된다.

const char name[] = "V2LLAIN";
const char* ptrname = name;

 

그런데 배열을 값 전달 매개변수로 받는 template에 전달하면 어떻게 될까?

우선 배열형식의 함수 매개변수라는 것은 없다!

물론 아래와 같은 구문 자체는 가능하다.

void myFunc(int param[]);

하지만 이 경우 배열선언은 하나의 포인터 선언으로 취급되므로 사실상 아래와 동일하게 취급된다.

void myFunc(int *param);  // 위와 동일한 함수

 

따라서 아래 예시를 보면 name은 배열이지만 T는 const char*로 연역되어 버린다.

const char name[] = "V2LLAIN";

template<typename T>
void f(T param);      
f(name);             // name은 배열이지만 T는 const char*로 연역됨

 

 

그런데 한가지 교묘한 요령이 있는데, 함수의 매개변수를 진짜 배열로 선언은 못하지만,

배열에 대한 참조로 선언할 수는 있다!

 

즉, 다음과 같이 템플릿 f가 인수를 참조로 받도록 수정하고 함수에 배열을 전달하면

T에 대해 연역된 형식은 배열의 실제형식이 된다!

const char name[] = "V2LLAIN";

template<typename T>
void f(T& param);      // 참조 전달 매개변수가 있는 템플릿
f(name);               // 배열을 f에 전달

 

배열에 대한 참조선언을 이용하면 배열에 담긴 원소들의 개수를 연역하는 템플릿을 만들 수 있다!

 

[item 15]에서 더 자세히 나오지만 이 함수를 constexpr로 선언하면 함수 호출의 결과를 컴파일 도중 사용할 수 있다!

그렇게 되면 기존 배열과 같은 크기의 새 배열을 선언하는 것이 가능하게 된다.

template<typename T, std::size_t N>

constexpr std::size_t arraySize(T (&) [N]) noexcept { return N; }

int Vals[] = {1, 2, 3, 4 ,5, 6, 7};  // Vals의 원소개수는 7
int mapVals[arraySize(Vals)];  // mapVals의 원소 개수 역시 7개

/*물론, modern C++ 개발자라면 std::array나 vector를 더 선호할 것이다.*/
std::array<int, arraySize(Vals)> mapVals;  // mapVals의 크기는 7

이때, arraySizenoexcept 선언한 것은 컴파일러가 더 나은 코드를 산출하는데 도움을 주기 위한 것이다. (item 14)

 

 

물론, C++에서 포인터로 붕괴하는 것이 배열만 있는 것은 아니다.

앞에서 배열에 대한 형식연역과 관련된 모든것은 함수포인터로의 붕괴에 적용된다.

void someFunc(int, double);

template<typename T>
void f1(T param);       // f1의 param은 값 전달 방식

template<typename T>
void f2(T& param);      // f2의 param은 참조 전달 방식

f1(someFunc);           // param은 함수포인터로 연역됨 // void (*)(int, double)
f2(someFunc);           // param은 함수참조로 연역됨   // void (&)(int ,double)

 

 

 

 

 

 

◈ Remind ◈

- 템플릿형식 연역 중 참조형식의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
- 보편참조 매개변수에 대한 형식 연역과정에서 l-value들은 특별하게 취급된다.
- 값 전달방식의 매개변수에 대한 형식연역과정에서 const, volatile 인수는 비 const, 비 volatile 인수로 취급된다.
- 템플릿형식 연역과정에서 배열이나 함수이름에 해당하는 인수는 포인터로 붕괴한다.
  단, 그런 인수가 참조를 초기화하는데 쓰이는 경우, 포인터로 붕괴하지 않는다.

※ C++의 버전변화

C++98, C++03  ==>  C++11  ==>  C++14 ...

 

C++11: 기존 C++98에서 람다 표현식을 지원한 것

C++14: 일반화된 함수반환 형식 유추를 지원한 것

 

 

※ C++11/14 문법적 변경 사항

1. 초기화 리스트 및 초기화 방법의 통합

2. 새로운 타입의 추가 : long long형 정수

3. 새로운 스마트 포인터 추가 

4. 널 포인터 상수 추가 : nullptr

5. 열거체의 범위 지정

6. 자동 타입 변환 : auto

7. 타입 변환 연산자 추가 : explicit

8. 범위 기반 for 문 추가

9. 람다 함수와 람다 표현식 추가

 

 

※ C++11/14  표준 라이브러리 변경 사항

1. 확장할 수 있는 난수 생성기의 추가 : random 헤더 파일

2. 튜플 템플릿의 추가: tuple 헤더 파일

3. 정규 표현식의 추가 : regex 헤더 파일

4. 다중 프로그래밍을 위한 스레드의 지원 : thread_local 키워드, automic 헤더 파일

5. 임베디드 프로그래밍을 위한 저수준 프로그래밍 지원

 

 

※ C++11의 move semantics

C++11에서 가장 널리 쓰이는 기능은 바로 이동 의미론이다. (move semantics)

이동의미론l-value와 r-value가 구분된다는 점에서 유용하다.

l-value: 이름, 포인터, 왼값 참조를 통해 지칭할 수 있는 객체 (주소를 취할 수 있음)

r-value:이동연산이 가능한 객체를 가리킴 (함수가 돌려준 임시객체)

class Widget {
public:
    Widget (Widget&& rhs);  // rhs의 형식은 r-value참조, rhs 자체는 l-value
};

여기서 유의할 점은 rvalue 참조 형식의 매개변수지만 매개변수자체는 lvalue이다.

Widget의 이동생성자 안에서 rhs의 주소를 취하고 있기 때문이다.

 

 

※ 복사생성자를 통해 생성된 복사본 vs 이동생성자를 통해 생성된 복사본

이 둘을 구분하는 용어는 없다. 아래 예시를 보면

대체로 rvalue 복사는 이동생성자로 생성되고 lvalue 복사는 복사생성자로 생성된다.

호출지점에서 함수에 전달하는 표현식을 argument, 인자라 하고 

인수는 함수의 매개변수, parameter를 초기화하는데 사용된다.

매개변수는 l-value이지만 매개변수의 초기화에 쓰이는 인수는 l,rvalue 둘 다 가능하다.

 

 

 

 

 

 

 

 

※  C++과 람다 표현식

람다표현식으로 만들어진 함수객체를 closure라 부른다.

람다표현식과 그로부터 생성된 클로저를 구분해야하는 경우는 드물다.

또한 함수템플릿과 템플릿함수를 구분하는 경우도 드물다. (클래스 템플릿과 템플릿 클래스도 마찬가지)

 

C++에서 함수는 크게 2가지로 나뉜다.

- 일반함수(General Function): 일반적인 반환타입을 갖는 함수

- 무명함수(Anonymous Function): 람다(Lambda)이지만 함수객체처럼 함수의 이름없이 사용가능한 함수

int main() {
    // 람다식 선언
    auto Func1 = [] {
        cout << "Hello, Lambda" << endl;
    };
    
    // 람다식 인수로 int n 선언
    auto Func2 = [](int n){
        cout << "val = " << n << endl;   
    };
    
    Func2(2);
}

람다는 일반 함수처럼 사전에 선언문과 정의문 없이 하나의 문장 || 다수의 문장으로 직접 정의하고 실행시킬 수 있다.

이런 람다는 1~2번 작업을 위한 임시적인 함수를 만드는데 적합하다.

 

 

※ 람다 만드는 법

[캡쳐절] { 본문 }
[캡쳐절] (인수) { 본문 }
[캡쳐절] (인수)->반환타입{ 본문 }
[캡쳐절] (인수) mutable(옵션) constexpr(옵션) 예외(옵션) 속성(옵션) -> 반환타입{ 본문 }

마지막은 C++17이후 추가된 람다형식이다. 이를 아래와 같이 표현할 수 있다.

[캡쳐 리스트](인수) mutable(optional) constexpr(optional) 예외속성 -> 반환데이터타입{ 본문 }

 

 

 

1. 캡쳐절 (Capture Clause)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

2. 인수 리스트 (Parameter List)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

3. mutable (옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

4. constexpr (옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

5. throw (옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

6. Attribute(옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

7. 반환 데이터 타입 (옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

8. 본문

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

 

+ Recent posts