생성자를 public이 아닌 곳에 두면 “누가 이 객체를 만들 수 있는가”를 클래스가 직접 정할 수 있다. protected 생성자는 파생 클래스만 기반 부분을 만들게 해서, 순수 가상 함수 없이도 기반 클래스를 단독으로 생성하지 못하게 한다. private 생성자는 팩토리 함수나 싱글턴처럼 생성 경로를 하나로 강제할 때 쓴다. 이 글에서는 g++ 13.3으로 접근 제어된 생성자의 규칙, make_unique/make_shared와의 충돌과 passkey 관용구, protected 소멸자를 확인한다.
이 시리즈의 다른 글
- C++ 고급 문법 (14) — 형변환(Conversion)
- C++ 고급 문법 (15) — new 연산자(new Expression)
- C++ 고급 문법 (17) — private 상속(private Inheritance)
- C++ 고급 문법 (18) — 업캐스팅(Upcasting)
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 비가상 소멸자는 기반 포인터로 지우는 실수를 컴파일 단계에서 막는다.