По сути, это все сводится к тому, какой дизайн выбрать из следующих трех:
Дизайн
Отказ от ответственности : этот пост не поощряет использование спецификаций исключенийили исключения по этому вопросу.Ошибки могут быть эквивалентно сообщены с использованием кодов ошибок, если вы хотите.Спецификации исключений, используемые здесь, просто предназначены для иллюстрации, когда различные ошибки могут возникать с использованием краткого синтаксиса.
Дизайн 1
Это наиболее повторяющийся дизайн, и он совершенно неRAII.Конструктор просто переводит объект в какое-то устаревшее состояние, и каждый экземпляр должен инициализироваться вручную после завершения построения.
class SecureStream
{
public:
SecureStream();
void initialize(Stream&,const Key&) throw(InvalidKey,AlreadyInitialized);
std::size_t get( void*,std::size_t) throw(NotInitialized,IOError);
std::size_t put(const void*,std::size_t) throw(NotInitialized,IOError);
};
Плюсы :
- Пользователи имеютконтроль, когда вызывать «тяжелый» процесс инициализации
- Объект может быть создан до того, как ключ существует.Это важно для таких сред, как COM, где все объекты должны иметь конструктор по умолчанию (
CoCreateObject()
не позволяет пересылать дополнительные аргументы конструктору объекта).Иногда все еще есть обходные пути, такие как строитель объект.
Минусы :
- Объекты должны быть проверены на наличиеустаревшее состояние перед использованием объекта.Этот может быть принудительно установлен объектом, возвращая код ошибки или выбрасывая исключение.Лично я ненавижу объекты, которые позволяют мне использовать их, и, похоже, просто игнорирую мои вызовы (например, неудавшийся
std::ostream
).
Дизайн 2
ЭтоRAII прим.Убедитесь, что объект пригоден для использования на 100% без дополнительных артефактов (например, вручную вызывая stream.initialize(...);
для каждого экземпляра.
class SecureStream
{
public:
SecureStream(Stream&,const Key&) throw(InvalidKey);
std::size_t get( void*,std::size_t) throw(IOError);
std::size_t put(const void*,std::size_t) throw(IOError);
};
Pros :
- объект всегда можно считать действительным. Это намного проще в использовании.
Минусы :
- Конструктору может потребоваться много времени для выполнения.
- Все необходимые аргументы должны быть доступны при построении экземпляра. Это иногда было проблемой для меня, особенно если большинство других объектов в базе кода используютdesign # 1.
Design 3
Отчасти компромисс между двумя предыдущими случаями. Пока не инициализируйте, но другие методы лениво вызывают внутренний .initialize(...)
метод при необходимости.
class SecureStream
{
public:
SecureStream(Stream&,const Key&);
std::size_t get( void*,std::size_t) throw(InvalidKey,IOError);
std::size_t put(const void*,std::size_t) throw(InvalidKey,IOError);
private:
void initialize() throw(InvalidKey);
};
Плюсы :
- Почти так же легко использовать, как дизайн # 1. Почти (см. ниже).
Минусы :
- Если этап инициализации может завершиться неудачно, он может теперьfail везде, где есть первый вызов любого из открытых методов .Правильная обработка ошибок для этого сценария чрезвычайно трудна.
Обсуждение
Если вы абсолютно должны заплатить за инициализацию для каждого экземпляра, то дизайн #1 не может быть и речи, так как это приводит к большему количеству ошибок в программном обеспечении.
Вопрос только в , когда для оплаты стоимости инициализации.Вы предпочитаете платить авансом или при первом использовании?В большинстве сценариев я предпочитаю платить авансом, потому что я не хочу предполагать, что пользователи могут обрабатывать ошибки позже в программе.Однако в вашей программе может быть определенная семантика потоков, и вы не сможете остановить потоки во время создания (или, наоборот, во время использования).
В любом случае вы все равно можете получить преимуществадизайна # 3 с использованием динамического выделения класса в дизайне № 2.
Заключение
По сути, если единственная причина, по которой вы колеблетесь, это какой-то философский идеал, когда конструкторы выполняются быстро, я быпросто используйте чистый дизайн RAII.