C++ 고급 문법 (16) — protected 생성자(protected Constructor)

생성자를 public이 아닌 곳에 두면 “누가 이 객체를 만들 수 있는가”를 클래스가 직접 정할 수 있다. protected 생성자는 파생 클래스만 기반 부분을 만들게 해서, 순수 가상 함수 없이도 기반 클래스를 단독으로 생성하지 못하게 한다. private 생성자는 팩토리 함수나 싱글턴처럼 생성 경로를 하나로 강제할 때 쓴다. 이 글에서는 g++ 13.3으로 접근 제어된 생성자의 규칙, make_unique/make_shared와의 충돌과 passkey 관용구, protected 소멸자를 확인한다.

이 시리즈의 다른 글

protected 생성자

#include <cstdio>

class Shape {                         // 순수 가상 함수 없이도 직접 생성을 막는다
protected:
    explicit Shape(const char* n) : name_(n) {}
    const char* name_;
public:
    const char* name() const { return name_; }
};

class Circle : public Shape {
public:
    Circle() : Shape("circle") {}     // 파생 클래스 생성자는 호출할 수 있다
};

int main()
{
    Circle c;
    std::printf("%s\n", c.name());
    // Shape s("raw");                // 주석 해제 시 아래 에러
}
$ g++ -std=c++20 -Wall -o b base.cpp
$ ./b
circle

주석 처리한 줄을 풀면 외부에서 기반 클래스를 직접 만들 수 없다는 에러가 난다.

$ g++ -std=c++20 -Wall -c base_err.cpp 2>&1 | grep -E 'error'
base_err.cpp:20:18: error: ‘Shape::Shape(const char*)’ is protected within this context

파생 클래스도 독립 객체는 못 만든다

class Shape {
protected:
    Shape() = default;
};

class Circle : public Shape {
public:
    Circle() : Shape() {}             // 자기 기반 클래스 부분 생성: OK
    static Shape make_plain()
    {
        return Shape();               // 독립된 Shape 객체 생성: protected 접근 규칙상 불가
    }
};
$ g++ -std=c++20 -Wall -c other.cpp 2>&1 | grep -E 'error|protected within'
other.cpp:11:22: error: ‘constexpr Shape::Shape()’ is protected within this context

protected 멤버는 파생 클래스 자신의 기반 부분을 통해서만 접근할 수 있다. 파생 클래스 안이라도 별개의 Shape 객체를 만드는 것은 허용되지 않는다.

생성자 접근생성 가능한 곳용도
public어디서나일반 클래스
protected파생 클래스 생성자(기반 부분), friend단독 생성 금지 기반 클래스, 믹스인
private클래스 자신의 멤버, friend팩토리, 싱글턴
= delete없음특정 형태의 생성 금지

private 생성자와 팩토리

#include <cstdio>
#include <memory>
#include <string>

class Connection {
    std::string host_;
    explicit Connection(std::string h) : host_(std::move(h)) {}   // private: 팩토리로만 생성
public:
    static std::unique_ptr<Connection> open(const std::string& host)
    {
        if (host.empty()) return nullptr;                        // 생성 전 검증
        return std::unique_ptr<Connection>(new Connection(host));
    }
    const std::string& host() const { return host_; }
};

int main()
{
    auto c = Connection::open("db.local");
    auto bad = Connection::open("");
    std::printf("c=%s bad=%p\n", c->host().c_str(), static_cast<void*>(bad.get()));
}
$ g++ -std=c++20 -Wall -o f factory.cpp
$ ./f
c=db.local bad=(nil)

생성 전에 인수를 검증하고 실패를 nullptr로 돌려줄 수 있다. 다만 std::make_unique는 자기 내부에서 생성자를 부르기 때문에 private 생성자에 접근하지 못한다.

#include <memory>
#include <string>

class Connection {
    explicit Connection(std::string) {}
public:
    static std::unique_ptr<Connection> open(const std::string& host)
    {
        return std::make_unique<Connection>(host);   // make_unique 내부에서 생성자를 부른다
    }
};

int main() { Connection::open("x"); }
$ g++ -std=c++20 -Wall -c makeunique.cpp 2>&1 | grep -E 'error' | head -2
/usr/include/c++/13/bits/unique_ptr.h:1070:30: error: ‘Connection::Connection(std::string)’ is private within this context

passkey 관용구

생성자를 public으로 두되 private 중첩 타입을 인수로 요구하면, 클래스 밖에서는 그 인수를 만들 수 없어 사실상 private 생성자가 된다. make_shared는 받은 Key를 전달만 하므로 그대로 쓸 수 있다.

#include <cstdio>
#include <memory>
#include <string>

class Connection {
    struct Key { explicit Key() = default; };          // 외부에서 만들 수 없는 열쇠 타입
    std::string host_;
public:
    Connection(Key, std::string h) : host_(std::move(h)) {}   // public이지만 Key 없이는 호출 불가

    static std::shared_ptr<Connection> open(const std::string& host)
    {
        return std::make_shared<Connection>(Key{}, host);     // make_shared 사용 가능
    }
    const std::string& host() const { return host_; }
};

int main()
{
    auto c = Connection::open("cache.local");
    std::printf("%s use_count=%ld\n", c->host().c_str(), c.use_count());
}
$ g++ -std=c++20 -Wall -o pk passkey.cpp
$ ./pk
cache.local use_count=1
#include <memory>
#include <string>

class Connection {
    struct Key { explicit Key() = default; };
public:
    Connection(Key, std::string) {}
};

int main()
{
    Connection c({}, "x");                               // Key를 만들 수 없다
}
$ g++ -std=c++20 -Wall -c passkey_err.cpp 2>&1 | grep -E 'error' | head -2
passkey_err.cpp:12:25: error: converting to ‘Connection::Key’ from initializer list would use explicit constructor ‘constexpr Connection::Key::Key()’

Key의 기본 생성자가 explicit이 아니면 {}로 우회할 수 있으므로 explicit이 필수다.

싱글턴

#include <cstdio>

class Registry {
    Registry()  { std::puts("  Registry created"); }
    ~Registry() { std::puts("  Registry destroyed"); }
public:
    Registry(const Registry&) = delete;
    Registry& operator=(const Registry&) = delete;

    static Registry& instance()
    {
        static Registry r;          // 처음 호출 시 생성, 스레드 안전 (C++11)
        return r;
    }
    int next_id() { return ++id_; }
private:
    int id_ = 0;
};

int main()
{
    std::puts("main start");
    std::printf("%d\n", Registry::instance().next_id());
    std::printf("%d\n", Registry::instance().next_id());
    std::puts("main end");
}
$ g++ -std=c++20 -Wall -o sg singleton.cpp
$ ./sg
main start
  Registry created
1
2
main end
  Registry destroyed

함수 안 static 지역 변수는 처음 호출될 때 생성되고, C++11부터 이 초기화는 스레드 안전이 보장된다(Meyers singleton). 소멸은 main이 끝난 뒤에 일어난다.

protected 소멸자

struct Mixin {                     // 기반 포인터로 다루지 않을 믹스인
protected:
    Mixin() = default;
    ~Mixin() = default;            // protected 비가상 소멸자: 기반 포인터로 delete 금지
};

struct Widget : Mixin {};

int main()
{
    Widget* w = new Widget;
    Mixin* m = w;
    delete m;                      // 컴파일 에러로 막힌다
}
$ g++ -std=c++20 -Wall -c pdtor.cpp 2>&1 | grep -E 'error'
pdtor.cpp:13:12: error: ‘constexpr Mixin::~Mixin()’ is protected within this context

기반 포인터로 다룰 일이 없는 클래스는 소멸자를 protected 비가상으로 두면 기반 포인터를 통한 delete가 컴파일 단계에서 막힌다. 다형적으로 쓸 기반 클래스는 가상 소멸자 편의 규칙을 따른다.

주의사항

상황문제대응
private 생성자 + make_unique/make_shared접근 에러unique_ptr(new T) 또는 passkey
passkey 타입의 기본 생성자에 explicit 누락{}로 외부 생성 가능explicit Key() = default;
복사 생성자를 지우지 않은 싱글턴auto r = instance();로 사본 생성복사·이동 = delete
싱글턴을 다른 정적 객체 소멸자에서 사용소멸 순서 문제로 이미 파괴된 객체 접근소멸자에서 싱글턴 사용 금지 또는 의도적 누수(new 후 미해제)
protected 생성자만으로 추상화파생 클래스가 아무 기능 없이도 생성 가능인터페이스 강제가 목적이면 순수 가상 함수

마무리

  • protected 생성자는 파생 클래스의 기반 부분 생성만 허용하고, 파생 클래스 안에서도 별개 객체 생성은 막는다.
  • private 생성자는 팩토리·싱글턴으로 생성 경로를 강제하며, make_*와 함께 쓰려면 passkey 관용구가 필요하다.
  • protected 비가상 소멸자는 기반 포인터로 지우는 실수를 컴파일 단계에서 막는다.

참고

답글 남기기