问题
I have a class called DBDriver that handles communication with a given table in a database. Its public entry point is a function template called execute_query(), which executes SELECT queries. Upon calling this function, some database logic is performed, and then a provided container (of the template type) is populated with results. This looks something like the following:
class DBDriver {
...
template <typename CONT_T>
void execute_query(const std::string& query, CONT_T& container);
...
};
template <typename CONT_T>
void DBDriver::execute_query(const std::string& query, CONT_T& container) {
DBCursor& cursor = ... // some database logic here
populate_container(container, cursor);
}
Of course, the above will not compile, as populate_container() is not defined in DBDriver.
DBDriver should be purely virtual and have several classes derive from it (one for each database table involved). Each derived class will define its own overloads of populate_container(), one for each relevant container type. This will look something like the following:
class SampleTableDBDriver : public DBDriver {
// ...
populate_container(const ContainerTypeOne& container, DBCursor& cursor);
populate_container(const ContainerTypeTwo& container, DBCursor& cursor);
// ...
};
My original attempt at this was unsuccessful, as I would have needed to define a virtual function template in DBDriver to serve as an entry point to a derived class' populate_container() overloads. (Of course such a thing does not exist in C++, hence my issue.)
Is there a cleaner, idiomatic solution for this type of problem?
回答1:
The reason why execute_query is a template function is you need a generic container. What if you define a Interface for the container?
class IContainer
{};
Template Functions cannot be virtual. Therefore, you can use the Template Method Design Pattern.
class DBDriver
{
public:
void execute_query(const std::string& query, IContainer **_ppContainer);
{
DBCursor& cursor = ... // some database logic here
populate_container(_ppContainer, cursor);
}
virtual void populate_container(IContainer **_ppContainer, DBCursor &_dbCursor) = 0;
};
And let every derived class implement populate_container and also provide their custom Container.
class SampleTableDBDriver : public DBDriver
{
public:
class ContainerTypeOne : public IContainer
{};
void populate_container(IContainer **_ppContainer, DBCursor &_dbCursor)
{
ContainerTypeOne *pContainer = new ContainerTypeOne();
//....
(*_ppContainer) = pContainer;
}
};
SampleTableDBDriver oSampleDriver;
IContainer *pContainer = NULL;
std::string szQuery = // some query ;
oSampleDriver.execute_query(szQuery, &pContainer);
if(pContainer != NULL)
{
SampleTableDBDriver::ContainerTypeOne *pSampleDriverContainer =
dynamic_cast<SampleTableDBDriver::ContainerTypeOne*>(pContainer);
//use pSampleDriverContainer
}
Edit: For supporting multiple containers.
In your original design the populate_container seems to be overloaded in derived classes. In that case you would still pass the exact container from outside while calling execute_query.
Same thing can be done with this Template Method design. Then, you will need to decipher the type of container inside the populate_container function as follows:
New signature : int populate_container(IContainer *_pContainer, DBCursor &_dbCursor)
int populate_container(IContainer *_pContainer, DBCursor &_dbCursor)
{
if(dynamic_cast<ContainerTypeOne *>(_pContainer) != NULL)
{
ContainerTypeOne *pContainerOne = _pContainer;
//populate the result by using pContainerOne
return 1;
}
if(dynamic_cast<ContainerTypeTwo *>(_pContainer) != NULL)
{
ContainerTypeOne *pContainerTwo = _pContainer;
//populate the result by using pContainerTwo
return 1;
}
//no, I do not support the container you passed.
return 0;
}
SampleTableDBDriver oSampleDriver;
SampleTableDBDriver::ContainerTypeOne oSampleContainerTypeOne;
std::string szQuery = // some query ;
if(oSampleDriver.execute_query(szQuery, &oSampleContainerTypeOne) != 0)
{
//use oSampleContainerTypeOne;
}
回答2:
You have options to choose from:
If you don't need populate_container() to be a member of DBDriver and can extend (or define) container interface, just implement
ContainerTypeOne::populate(DBCursor& cursor);Make populate_container() a friend (in case you need access to private members of DBDriver):
template <typename CONT_T> friend void execute_query(const std::string& query, CONT_T& container);Make populate_container() a non-member template function (if you don't need access to private members of DBDriver)
回答3:
One way to do this is to employ the separation of concerns principle.
Querying databases goes to its own class hierarchy, and populating container goes to its own separate class hierarchy. These two hierarchies know nothing about each other, e.g. DBDriverThirteen knows nothing about ContainerFortyTwo. The two hierarchies only touch at their roots, i.e. DBDriver (and so its every derived class) knows about IContainerPopulator but nothing about any specific containers.
You may have a template that builds a specific ContainerPopulator for each container type. Assume for simplicity that you only need to support standard containers of strings that implement push_back.
struct IContainerPopulator {
virtual void push_back(const std::string&) = 0;
};
template <class CONT_T>
struct ContainerPopulator : IContainerPopulator {
StandardContainerPopulator (CONT_T& cont) : cont(cont) {}
void push_back(const std::string& s) override { cont.push_back(s); }
private:
CONT_T& cont;
};
Now you can do this
template <typename CONT_T>
void execute_query(const std::string& query, CONT_T& container) {
execute_query_adapted(query, ContainerPopulator<CONT_T>(container));
}
// no template!
virtual void execute_query_adapted(const std::string&,
IContainerPopulator&&) = 0;
By now you have probably recognised that IContainerPopulator is nothing but a specialised poor man's function binder. Why write another one if we have pretty good support in the language and its standard library? If you only need to support push_back, you can alternatively do this:
template <typename CONT_T>
void execute_query(const std::string& query, CONT_T& container) {
execute_query_adapted2(query,
[&](const std::string& s){container.push_back(s);});
}
// no template
virtual void execute_query_adapted2(const std::string&,
std::function<void(const std::string&)>) = 0;
Now if you need more than just push_back (or any fixed set of functions) and/or more than std::string (or any fixed set of types), things can get really hairy. More information would be needed about potential implementations of populate_query.
回答4:
As long as you don't mind using RTTI (or Boost.TypeIndex, which doesn't require RTTI), you can use type erasure and void* to get what you want. It's a bit dirty, but it does the job.
#include <functional>
#include <typeindex>
#include <unordered_map>
class driver {
public:
template <typename Container, typename Populator>
void register_populator(Populator populator) {
populators[type_index<Container>()] = [populator](void* v) {
Container& container = *static_cast<Container*>(v);
populator(container);
};
}
template <typename Container>
void execute(Container& container) {
auto it = populators.find(type_index<Container>());
if (it != populators.end()) {
it->second(&container);
}
}
private:
template <typename T>
static std::type_index type_index() {
return std::type_index(typeid(std::remove_cv_t<T>));
}
std::unordered_map<std::type_index, std::function<void (void*)>> populators;
};
Then use like so:
#include <vector>
#include <iostream>
int main() {
driver d;
d.register_populator<std::vector<int>>([](std::vector<int>&) {
std::cout << "Populate vector<int>\n";
});
d.register_populator<std::vector<float>>([](std::vector<float>&) {
std::cout << "Populate vector<float>\n";
});
std::vector<int> int_vector;
std::vector<float> float_vector;
d.execute(int_vector);
d.execute(float_vector);
}
Output:
Populate vector<int>
Populate vector<float>
来源:https://stackoverflow.com/questions/41844099/accessing-derived-class-member-functions-in-base-template-function