#c #vector #list-initialization
#c #вектор #список-инициализация
Вопрос:
Вот рабочий код, который я хотел бы упростить вызов. Давайте начнем с вызова:
Config c2( {
new ConfigPairInt( "one", 1 ),
new ConfigPairDouble( "two", 2.0 ),
new ConfigPairString( "three", "3" )
} );
Первое, от чего я хотел бы избавиться, это new
‘s . (Если мне это new
нравится, я полагаю, это утечка памяти?) Однако, когда я заставляю Config
конструктор принимать vector<ConfigPairamp;>
и удалять эти new
, я получаю страницы ошибок, которые для меня не имеют смысла.
Второе, от чего я хотел бы избавиться, — это имена классов типов. Если я вместо этого переключусь на инициализаторы с фигурными скобками, то компилятор знает, что список с инициализатором является ConfigPair*
, и поэтому все, что он видит, должно быть инициализатором для ConfigPair
или одного из его подклассов.Но действительно ли компилятор ищет конструкторы подклассов? По-видимому, нет. (У меня есть идея, которая заключается в том, чтобы заставить ConfigPair выполнять работу всех своих подклассов и предоставить ему три разных конструктора. Он больше не будет полиморфным. И, не будучи полиморфным, я мог бы также избавиться от new
, потому что вектор может быть этого ConfigPair
класса, а ConfigPair*
не того, который необходим из-за полиморфизма. Однако полиморфизм, похоже, идеально подходит для решения проблемы, поэтому я не хочу отказываться от него.)
Третья вещь, от которой я хотел бы избавиться, — это фигурные скобки вокруг vector
инициализатора. Я думаю, что мог бы сделать это с помощью пакета параметров, но я не могу получить результирующий список параметров подкласса для инициализации вектора.
class ConfigPair {
public:
ConfigPair( const char* pszName_in ) :
pszName( newstring( pszName_in ) )
{
};
virtual ~ConfigPair() {
free( (char*) pszName );
};
const char* pszName;
};
class Config {
public:
Config( std::vector<ConfigPair*> apcpair )
{
for( ConfigPair* pcpair: apcpair )
// do something
}
};
class ConfigPairInt : public ConfigPair {
public:
ConfigPairInt( const char* pszName_in, int iValue_in ) :
ConfigPair( pszName_in ),
iValue( iValue_in )
{
};
int iValue;
};
class ConfigPairDouble : public ConfigPair {
public:
ConfigPairDouble( const char* pszName_in, double dValue_in ) :
ConfigPair( pszName_in ),
dValue( dValue_in )
{
};
double dValue;
};
class ConfigPairString : public ConfigPair {
public:
ConfigPairString( const char* pszName_in, const char* pszValue_in ) :
ConfigPair( pszName_in ),
pszValue( newstring( pszValue_in ) )
{
};
virtual ~ConfigPairString() {
free( (char*) pszValue );
};
const char* pszValue;
};
Комментарии:
1.
Config( std::vector<ConfigPair*> apcpair )
кажется, лучше всего подходит. Ваш класс bas должен быть абстрактным или полностью абстрактным интерфейсом, чтобыvirtual
с ним можно было вызывать функции. Также рассмотрите возможность создания параметраconst
до тех пор, пока вы не хотите ничего там менять.2. Другое дело, что вам не обязательно создавать новый производный класс для каждой возможной пары разных типов, вы можете рассмотреть возможность использования
std::variant
илиstd::any
для решения этой проблемы.3. Что касается утечек памяти, рассмотрите возможность использования интеллектуального указателя вместо необработанных указателей. Они будут автоматически управлять памятью.
4. «не могли бы вы привести более длинный пример этого?» Ну, да. Но вам, вероятно, следует уточнить ваши требования в вопросе. Я мог бы начать с необработанного черновика.
5. Хорошо, я считаю, что это все для первого выстрела. Смотрите Ответ ниже.
Ответ №1:
Вы можете избавиться от использования new
и vector
достаточно легко, используя вариационный шаблон для Config
конструктора, например:
class Config
{
public:
template<typename... Ts>
Config(const Tsamp;... args)
{
const ConfigPair* arr[] = {amp;args...};
// use arr as needed...
for( const ConfigPair *arg: arr ) {
// do something with arg...
}
}
};
int main()
{
Config c2(
ConfigPairInt("one", 1),
ConfigPairDouble("two", 2.0),
ConfigPairString("three", "3")
);
// use c2 as needed...
return 0;
}
Вы просто не можете избавиться от производных имен типов, если хотите использовать полиморфизм, поскольку компилятор должен знать, какие типы создавать. Само по себе использование инициализатора фигурных скобок не указывает компилятору, КАКОЙ тип создать:
Config c2(
{"one", 1}, // error: WHICH type?
{"two", 2.0}, // error: WHICH type?
{"three", "3"} // error: WHICH type?
);
Если вам нужен такой синтаксис, то полностью избавьтесь от полиморфизма, просто пусть ConfigPair
он сам обрабатывает любой тип значения, через std::variant
, std::any
, или эквивалентный.
Комментарии:
1.Вы просто не можете избавиться от производных имен типов, если хотите использовать полиморфизм, поскольку компилятор должен знать, какие типы создавать. Использование инициализатора фигурной скобки само по себе не указывает компилятору, КАКОЙ тип создавать: ну, в области видимости для
ConfigPair
или его подклассов есть только один конструктор, который соответствует этой сигнатуре. Это определенно то, что может сделать человек, и я думаю, что это то, что спецификация может потребовать от компилятора. Но если вы говорите, что в спецификации не указано, что компилятор должен это делать, тогда согласен и понял.2. Если
Config
конструктор принимаетConfiPair*
илиConfigPairamp;
в качестве входных данных, компилятор не будет автоматически искать и вызывать соответствующие производные конструкторы для вас, вы должны вызывать их явно самостоятельно.3. компилятор не будет автоматически искать и вызывать соответствующие производные конструкторы для вашего вздоха, может быть, в C 23? Я думаю, что это было бы технически возможно, поскольку в области видимости есть только один соответствующий конструктор подкласса, и его можно найти очень быстро…
4. Возможно, в этом конкретном примере , но не в целом , нет. Как вы думаете, что он будет делать, если найдет несколько подходящих конструкторов для заданного списка параметров? Как вы ожидаете, что он выберет, какой тип создавать? Итак, нет, просто, нет.
5. * Возможно, в этом конкретном примере, но не в целом, нет. Как вы думаете, что он будет делать, если найдет несколько подходящих конструкторов для заданного списка параметров? * То же самое, что он делает всякий раз, когда находит несколько возможных совпадений: выдает ошибку, что существует несколько возможных совпадений.
Ответ №2:
Вот сырой проект того, что я бы сделал в вашем случае (насколько описано), пояснения в комментариях:
using ValueType = std::variant<std::string,int,double>; // To get rid of lengthy
// type names, and
// have them
// change- and maintainable
// at a single point in your
// source code
class ConfigPair {
public:
ConfigPair( const std::stringamp; name_in, ValueType value ) :
// ^^^^^^^ Ditch "Hungarian notation" for heaven's sake!
name( name_in ) {}
virtual ~ConfigPair() {}; // Destructor doesn't need to do anything
// std::string manages memory already
std::string name; // Is compatible with const char* for initialization
ValueType value;
};
using PConfigPair = std::shared_ptr<ConfigPair>;
// or in case that Config should take full ownership of ConfigPair's
// using PConfigPair = std::shared_ptr<ConfigPair>;
class Config {
public:
Config( std::vector<PConfigPair> apcpair )
{
for( PConfigPair pcpair: apcpair )
// do something
}
// Maybe store the vector here as class member, if Config should take full
// ownership of those ConfigPair's
// std::vector<PConfigPair> config_pairs;
//
// In any case you don't need to care about manual memory management
};
Config c2( { std::make_shared<ConfigPair>("one",1), // std::make_unique alternatively
std::make_shared<ConfigPair>("two",2.0),
std::make_shared<ConfigPair>("three","3") } );
Документы:
- std::variant
- std::shared_ptr (альтернативно вы можете использовать
std::unique_ptr
, в зависимости от семантики владения)
Комментарии:
1. Большое вам спасибо. Я не использовал и даже не видел
variant
раньше. Я должен изучить это, в дополнение к тремConfigPair
, которые я упомянул, мне также понадобятся еще несколько, в том числе иерархический, в котором второй аргумент сам по себе является векторомConfigPair
подклассов. Вы видите, как добавить это в variant?2. Было
name( name_in )
бы более эффективно, какname( std::move( name_in ) )
?3. Вместо
variant
этого, я полагаю, я мог бы создатьConfigPair
шаблон и позволить пользователю указывать любой тип, поддерживающий assign и т. Д., В качестве аргумента.4. Я вижу:
make_shared
создаст объект в куче, но как только конструктор вернется, ссылок на объекты больше не будет, и их можно освободить. До сих пор я рассматривал только крупномасштабный дизайнmake_shared
и не видел такого небольшого тактического использования. Мне нравится это 10/10 с технической точки зрения, но это кажется большим шагом назад для истинной цели, которая упрощает использование объекта вызывающей стороной.5. @SwissFrank Как уже упоминалось, в качестве альтернативы вы можете использовать
std::unique_ptr
. Перемещение ничего не выигрывает по сравнениюconst
со ссылочным параметром. Альтернативой может быть шаблон, ноConfig
классу снова нужно знать о конкретных типах.
Ответ №3:
КАК указывают Реми и Панта в других ответах: вы просто не можете избавиться от имен производных типов, если хотите использовать полиморфизм, поскольку компилятор должен знать, какие типы создавать.Поскольку это наиболее типизируемый для вызывающего, его устранение является приоритетом. Таким образом, полиморфизм исключается.
Вместо этого то, что было виртуальным базовым классом для разных типов пар, теперь становится «швейцарским армейским ножом», способным выполнять работу своих подклассов. Он имеет различные конструкторы и дополнительные накладные расходы на перечисление, чтобы узнать, какой конструктор был вызван.
public:
ConfigPair( const char* pszName_in, int iValue_in ) {
cfgpair.type = TypeInt64;
cfgpair.pszName = newstring( pszName_in );
cfgpair.u.i = iValue_in;
}
ConfigPair( const char* pszName_in, double dValue_in ) {
cfgpair.type = TypeDouble;
cfgpair.pszName = newstring( pszName_in );
cfgpair.u.d = dValue_in;
}
ConfigPair( const char* pszName_in, const char* pszValue_in ) {
cfgpair.type = TypeSz;
cfgpair.pszName = newstring( pszName_in );
cfgpair.pszValue = newstring( pszValue_in );
}
Учитывая, что ConfigPair
теперь это единственный класс, передаваемый в Config
конструктор, означает, что осведомленность компилятора о требованиях к типу этого конструктора позволяет ему определять тип объекта при вызове. Таким образом, имя класса не используется.
Кроме того, поскольку содержимое массива теперь однородно, мы можем поместить сами ConfigPair
‘ы в массив, вместо того, чтобы (как это было изначально необходимо) хранить только указатели на объекты ConfigPair
подкласса. Таким образом, новая (или альтернативная и не требующая утечки памяти, но еще более подробная shared_pointer<ConfigPairXXX>()
нотация).
Config( const char* pszName_in, // name only used for debugging messages
std::vector<ConfigPair> acpair ) :
Config( pszName_in )
{
for ( const ConfigPair cpair: acpair )
cpair.Set( this );
}
И для вызывающего:
Config c2( { { "one", 1 },
{ "two", 2.0 },
{ "three", "3" } } );
Наконец, это оставляет внешние фигурные скобки для списка-инициализатора-списка. Я исследовал замену вектора пакетом параметров, но тогда я не могу понять, как заставить компилятор больше знать о типе, поэтому тип снова потребуется. Продолжить с этим было бы одним шагом вперед на десять шагов назад с точки зрения простоты для вызывающего, поэтому я все-таки придерживаюсь списка braced-initializer. (Если есть другая альтернатива, чтобы избавиться от этих внешних фигурных скобок, не требуя имен классов, пожалуйста, дайте мне знать.)