Мне нужно динамически создавать экземпляры класса Aggregate
на C++, который наследуется от нескольких базовых классов, где комбинация базовых классов может варьироваться в зависимости от условий выполнения. Я хочу избежать предварительного заполнения всех возможных комбинаций.
Вот упрощенная версия того, чего я пытаюсь достичь
У меня есть несколько базовых классов:
class CharBase
{
CharBase() { std::cout << "CharBase\n"; }
};
class UnsignedCharBase
{
UnsignedCharBase() { std::cout << "UnsignedCharBase\n"; }
};
class ShortBase
{
ShortBase() { std::cout << "ShortBase\n"; }
};
class IntBase
{
IntBase() { std::cout << "IntBase\n"; }
};
Я использую следующий шаблон класса Aggregate
для наследования от нескольких базовых классов.
template<typename... Bases>
class Aggregate : Bases…
{
Aggregate() { std::cout << "Aggregate\n"; }
};
Моя цель — создавать экземпляры классов Aggregate
по требованию на основе условий времени выполнения. Ожидаемый результат — создание экземпляра Aggregate
так, как если бы я его жестко запрограммировал, например:
new Aggregate<CharBase, UnsignedCharBase, ShortBase>
new Aggregate<UnsignedCharBase, IntBase>
Я понимаю, что шаблоны обрабатываются во время компиляции. Очевидно, я пытаюсь динамически решить, какую комбинацию базовых классов использовать во время выполнения, а затем создать экземпляр агрегатного класса, который наследуется от этих базовых классов.
давайте предположим, что вы можете, как вы планируете использовать его позже?
Путем анализа некоторых операторов из файла JSON во время выполнения.
Я имею в виду — давайте предположим, что вы объявили такой шаблон и создали экземпляр такого класса — как вы будете его использовать? вы вызовете какие-нибудь методы, передадите их куда-нибудь?
Я намерен вызывать методы, определенные в базовых классах, передавать экземпляры другим функциям, которые ожидают определенного поведения, обеспечиваемого базовыми классами, и т. д.
как вы определите, какие методы вы можете вызывать и каким методам передавать экземпляр такого класса?
Это похоже на работу для std::tuple.
@Kitiara «Я пытаюсь динамически решить, какую комбинацию базовых классов использовать во время выполнения, а затем создать экземпляр класса Aggregate
, который наследуется от этих базовых классов» — Извините, но C++ просто не работает таким образом. Ближе всего к этому можно подойти примерно так: if (useCharBase && useUnsignedCharBase && useShortBase) { Aggregate<CharBase, UnsignedCharBase, ShortBase> agg; doSomethingWith(agg); } else if (useUnsignedCharBase && useIntBase) { Aggregate<UnsignedCharBase, IntBase> agg; doSomethingWith(agg); } else ...
Очень быстро это становится очень некрасиво.
@Kitiara Это очень похоже на XY-проблему . Было бы полезно, если бы вы отредактировали свой вопрос, чтобы показать примеры того, как вы ожидаете использовать эти экземпляры классов. Однако в этой ситуации композиция может иметь больше смысла, чем наследование.
@IłyaBursov Опять из Json. Что вы получаете в ? Типовая безопасность? Шаблон класса Aggregate
обеспечивает безопасность типов, гарантируя, что можно вызывать только допустимые методы из включенных базовых классов.
@RemyLebeau Я бы не задавал этот вопрос, если бы хотел заняться композицией. Я не согласен называть проблемой XY Problem
, когда ее сложно решить.
@PetBecker Наверное. Возможно, шаблонный кортеж с задействованным std::index_sequence
@Китиара, попробуй реализовать пару комбинаций просто для проверки, и ты увидишь, к чему я иду.
@Kitiara Я не называю это проблемой XY, потому что это сложно. Я называю это так, потому что именно это я вижу в следующем: «вы пытаетесь решить проблему X и думаете, что решение Y сработает, но вместо того, чтобы спрашивать об X, когда у вас возникают проблемы, вы спрашиваете об Y». Мы не знаем, в чем заключается ваша проблема X (какова ваша цель с этими классами), но вы, кажется, думаете, что Y (динамическое указание базовых классов во время выполнения) является решением, хотя это не может быть так, поскольку C++ не делает этого. поддержите это. Поэтому вам нужно другое решение. Если бы мы знали проблему, которую вы хотите решить, мы могли бы помочь найти решение.
@RemyLebeau Пока мы говорим, я уже добился некоторого прогресса, так что не говорите мне, что C++ не может этого сделать.
@Китиара, ладно, неважно. Удачи в ваших усилиях.
База данных Oracle генерирует код на лету из операторов SQL, компилирует этот код и запускает его. Ваша программа также может динамически генерировать необходимый ей код, компилировать его и запускать во время выполнения.
@Eljay Я искал только решение на C++, но спасибо за идею.
Мой комментарий касался только решения C++, используя Oracle в качестве примера программы C++, которая использует решение C++ только для нужд SQL.
@Eljay О, я вижу, я неправильно понял.
Я преодолеваю ограничение в своем предыдущем ответе. Вы можете увидеть новый ответ ниже.
Новый ответ:
#include <array>
#include <iostream>
#include <memory>
#include <tuple>
class IAggregate
{
public:
virtual ~IAggregate() = default;
};
template<typename... Bases>
class Aggregate : public IAggregate, public Bases...
{
public:
Aggregate() { std::cout << "Aggregate\n"; }
virtual ~Aggregate() override = default;
};
// Base classes for demonstration
class CharBase
{
public:
CharBase() { std::cout << "CharBase display\n"; }
};
class UnsignedCharBase
{
public:
UnsignedCharBase() { std::cout << "UnsignedCharBase display\n"; }
};
class ShortBase
{
public:
ShortBase() { std::cout << "ShortBase display\n"; }
};
class IntBase
{
public:
IntBase() { std::cout << "IntBase display\n"; }
};
class FloatBase
{
public:
FloatBase() { std::cout << "FloatBase display\n"; }
};
// List of all base classes
using AllBases = std::tuple<CharBase, UnsignedCharBase, ShortBase, IntBase, FloatBase>;
constexpr std::size_t NumBases = std::tuple_size_v<AllBases>;
// Custom constexpr bitset class auto configured BITS_PER_WORD
template<std::size_t N, std::size_t BITS_PER_WORD>
class ConstexprBitset
{
public:
constexpr ConstexprBitset()
: bits {}
{
}
constexpr void set(std::size_t pos)
{
if (pos >= N)
throw std::out_of_range("Bit position out of range");
bits[pos / BITS_PER_WORD] |= (1ULL << (pos % BITS_PER_WORD));
}
constexpr bool test(std::size_t pos) const
{
if (pos >= N)
throw std::out_of_range("Bit position out of range");
return (bits[pos / BITS_PER_WORD] & (1ULL << (pos % BITS_PER_WORD))) != 0;
}
private:
std::array<unsigned long long, (N + BITS_PER_WORD - 1) / BITS_PER_WORD> bits;
};
constexpr std::size_t calculateOptimalBitsPerWord(std::size_t numBases)
{
return (numBases + 63) / 64; // Each unsigned long long covers 64 bits
}
constexpr std::size_t OptimalBitsPerWord = calculateOptimalBitsPerWord(NumBases);
template<int Mask, typename Tuple, typename Enable = void, typename... Types>
struct TypeExtractor;
template<int Mask, typename Tuple, typename... Types>
struct TypeExtractor<Mask, Tuple, std::enable_if_t<Mask == 0>, Types...>
{
using type = Aggregate<Types...>;
};
template<int Mask, typename Tuple, typename... Types>
struct TypeExtractor<Mask, Tuple, std::enable_if_t<Mask != 0>, Types...>
{
static constexpr ConstexprBitset<NumBases, OptimalBitsPerWord> mask = []()
{
ConstexprBitset<NumBases, OptimalBitsPerWord> bs;
for (std::size_t i = 0; i < NumBases; ++i)
if (Mask & (1 << i))
bs.set(i);
return bs;
}();
static constexpr std::size_t highestBit = []()
{
for (std::size_t i = NumBases; i > 0; --i)
if (mask.test(i - 1))
return i - 1;
throw std::out_of_range("No bits are set");
}();
using BaseType = std::tuple_element_t<highestBit, Tuple>;
using type = typename TypeExtractor<Mask & ~(1 << highestBit), Tuple, void, Types..., BaseType>::type;
};
template<int Mask>
using ExtractTypes = typename TypeExtractor<Mask, AllBases>::type;
using AggregatePtr = std::unique_ptr<IAggregate>;
template<int Mask>
AggregatePtr createAggregateInstance()
{
return std::make_unique<ExtractTypes<Mask>>();
}
// Helper to create a map of factory functions
template<int... Masks>
constexpr auto createFactoryMap(std::integer_sequence<int, Masks...>)
{
using FuncType = AggregatePtr (*)();
return std::array<FuncType, sizeof...(Masks)> {{&createAggregateInstance<Masks>...}};
}
// The factory map consists of AllBases tuple
constexpr auto factoryMap = createFactoryMap(std::make_integer_sequence<int, 1 << NumBases> {});
AggregatePtr createAggregate(int mask)
{
if (mask < factoryMap.size())
return factoryMap[mask]();
return nullptr;
}
int main()
{
for (int i = 1; i <= 3; ++i)
{
auto instance = createAggregate(i);
if (!instance)
std::cout << "Invalid combination for input: " << i << "\n";
std::cout << "\n";
}
return 0;
}
Старый ответ:
Вот это решение, которое мне удалось придумать. Единственное ограничение здесь — это highestSetBit
. Я пытался избавиться от этого, но не смог, буду признателен, если кто-нибудь сможет это понять.
#include <array>
#include <iostream>
#include <memory>
#include <tuple>
class IAggregate
{
public:
virtual ~IAggregate() = default;
};
template<typename... Bases>
class Aggregate : public IAggregate, public Bases...
{
public:
Aggregate() { std::cout << "Aggregate\n"; }
virtual ~Aggregate() override = default;
};
class CharBase
{
public:
static constexpr int Mask = 1 << 0;
CharBase() { std::cout << "CharBase display\n"; }
};
class UnsignedCharBase
{
public:
static constexpr int Mask = 1 << 1;
UnsignedCharBase() { std::cout << "UnsignedCharBase display\n"; }
};
class ShortBase
{
public:
static constexpr int Mask = 1 << 2;
ShortBase() { std::cout << "ShortBase display\n"; }
};
class IntBase
{
public:
static constexpr int Mask = 1 << 3;
IntBase() { std::cout << "IntBase display\n"; }
};
// List of all base classes
using AllBases = std::tuple<CharBase, UnsignedCharBase, ShortBase, IntBase>;
constexpr int highestSetBit(const int mask) // This is the only limitation in this design
{
for (int i = 31; i >= 0; --i)
if (mask & (1 << i))
return i;
return -1; // should never reach here for valid masks
}
template<int Mask, typename Tuple, typename Enable = void, typename... Types>
struct TypeExtractor;
template<int Mask, typename Tuple, typename... Types>
struct TypeExtractor<Mask, Tuple, std::enable_if_t<Mask == 0>, Types...>
{
using type = Aggregate<Types...>;
};
template<int Mask, typename Tuple, typename... Types>
struct TypeExtractor<Mask, Tuple, std::enable_if_t<Mask != 0>, Types...>
{
static constexpr int highestBit = highestSetBit(Mask);
using BaseType = std::tuple_element_t<highestBit, Tuple>;
using type = typename TypeExtractor<Mask & ~(1 << highestBit), Tuple, void, Types..., BaseType>::type;
};
template<int Mask>
using ExtractTypes = typename TypeExtractor<Mask, AllBases>::type;
using AggregatePtr = std::unique_ptr<IAggregate>;
template<int Mask>
AggregatePtr createAggregateInstance()
{
return std::make_unique<ExtractTypes<Mask>>();
}
// Helper to create a map of factory functions
template<int... Masks>
constexpr auto createFactoryMap(std::integer_sequence<int, Masks...>)
{
using FuncType = AggregatePtr (*)();
return std::array<FuncType, sizeof...(Masks)> {{&createAggregateInstance<Masks>...}};
}
// The factory map consist of AllBases tuple
constexpr auto factoryMap = createFactoryMap(std::make_integer_sequence<int, 1 << std::tuple_size_v<AllBases>> {});
AggregatePtr createAggregate(int mask)
{
if (mask < factoryMap.size())
return factoryMap[mask]();
return nullptr;
}
int main()
{
for (int i = 1; i <= 3; ++i)
{
auto instance = createAggregate(i);
if (!instance)
std::cout << "Invalid combination for input: " << i << "\n";
std::cout << "\n";
}
return 0;
}
Итак, технически вы создали экземпляр пустого IAggregate, планируете ли вы привести его к фактическому использованию?
@IłyaBursov Нет, он возвращается в IAggregate. Если вы проверите выходные данные, вы увидите, что агрегат создается вместе с базовыми классами.
переменная instance
— это указатель на IAggregate, можете ли вы показать пример ее использования?
@IłyaBursov Сам экземпляр может регистрироваться this
в контейнере с идентификатором или чем-то еще. Есть бесконечные возможности.
даже если вы его где-то зарегистрируете - вам все равно придется приводить, что исключает всю безопасность во время компиляции
Один из способов превратить значение времени выполнения в значение компиляции — использовать std::variant.
В вашем случае я бы использовал std::tuple для сохранения результата для «необязательного» типа.
template <typename T>
std::variant<std::tuple<>,
std::tuple<std::type_identity<T>>>
AsOptionalTypeVar(bool b)
{
if (b) {
return std::tuple<std::type_identity<T>>{};
} else {
return std::tuple<>{};
}
}
Помощник для преобразования кортежа в Aggregate
template <typename T> struct to_aggregate;
template <typename... Ts> struct to_aggregate<std::tuple<std::type_identity<Ts>...>>
{
using type = Aggregate<Ts...>;
};
Тогда вы можете позволить std::visit выполнить декартово произведение
std::visit(
[](auto... bases){
using all_base = decltype(std::tuple_cat(bases...));
using aggregate = typename to_aggregate<all_base>::type;
// Use aggregate type as you want
},
AsOptionalTypeVar<CharBase>(useCharBase),
AsOptionalTypeVar<UnsignedCharBase>(useUnsignedCharBase),
AsOptionalTypeVar<ShortBase>(useShortBase),
AsOptionalTypeVar<IntBase>(useIntBase)
);
Да, похоже, хорошее решение. Решение, которое я предлагаю, заключалось в предварительном заполнении всех комбинаций автоматически, а не вручную. Однако за это придется заплатить, и то, что вы предлагаете, намного лучше, спасибо.
Вы сказали, что вам нужно, но в чем заключается ваш вопрос? Шаблоны обрабатываются только во время компиляции, а не во время выполнения. Кроме того, ваши примеры
Aggregate<...>
представляют собой совершенно разные и несвязанные типы без общего базового класса, поэтому они не являются взаимозаменяемыми, если вы пытаетесь сохранить их, например, в одной переменной-указателе.