Alternative design to avoid dynamic_cast?
Let's say I have an interface Archive
and File
.
- Everyone is
File
guaranteed the availability of at leaststd::string name
. - Everyone
Archive
can havestd::vector<File*> Archive::list() const
his files. - Everyone
Archive
canArchive::extract(std::vector<File*> files)
.
Then I have ZipArchive
and ZipFile
, ZipFile
contains the archive offset and other implementation details. Then there is TarArchive
/ TarFile
, etc. Each of them fills with std::vector<File*> list() const
instances ZipFile
, TarFile
etc.
list()
is intended to give users the ability to select files to unpack. They pick elements from this vector, then pass that vector to extract()
.
At this point ZipArchive
it is necessary to assume that it passed the correct type and dynamic_cast<ZipFile*>(file)
to access the implementation details.
This is bad. It is acceptable? Are there alternatives?
source to share
As pointed out in the comments, you can move the retrieved interface from Archive
to File
. The archive will be returned std::vector<File*>
, but in reality each object will be, for example, ZipFile
and will know to which archive it belongs and what its type is, and will be able to call the correct extraction method.
As a result, you can have code without checking the archive type:
struct File;
struct Archive {
virtual std::vector<File*> fileList() = 0;
};
struct File {
File(std::string name_) : name(name_) {}
virtual void extract() = 0;
std::string name;
};
struct ZipFile;
struct ZipArchive: public Archive {
void extractFile(ZipFile& file);
virtual std::vector<File*> fileList();
};
struct ZipFile: public File {
ZipArchive* archive;
virtual void extract() { archive->extractFile(*this); }
ZipFile(std::string name_, ZipArchive* archive_) : File(name_), archive(archive_) {}
};
Full example: http://ideone.com/kAs5Jc
It can be more complicated if you want to extract many files in one call, but you can have an archive extractFile
, just remember this file, and then a special method in the class Archive
to extract all memorized files in one go. I think it might even be hidden under a fairly simple interface.
source to share
Yours ZipArchive
could search in your file list for the passed pointer. If it's there, it can either use a stored pointer (which already has a type ZipFile
) or a static_cast
passed pointer to ZipFile
(because you've proven its type). If the passed pointer is not in the list, it is clearly not a file belonging to this archive, so you can continue to handle errors.
You can also add a reverse type pointer Archive*
to each File
. A concrete implementation ZipArchive
can check if its one of its files is not a simple pointer compass.
void ZipArchive::extract(std::vector<File*> files)
{
for (auto file : files)
{
if (file->archive() == this)
{
// one of my files
auto zipFile = static_cast<ZipFile*>(file);
// do something with zipFile
}
else
{
// file is owned by some other archive
}
}
}
source to share
class Archive {
public:
static int registerArchiveType(const std::string &name) {
// generate a unique int for the requested name archive type
// and store it in a map or whatever
return uniqueInt;
}
int archiveType() const;
protected:
Archive(int type) : _type(type) {}
private:
int _type;
public:
virtual extract(std::vector<File*> files);
// your implementation details
};
class File {
public:
int archiveType() { return _archType; }
protected:
// force implementations to pass the same type
// they received from the call to Archive::registerArchiveType
File() {}
void setArchiveType(const std::string &archiveType) {
// multiple calls to registerArchiveType return the
// same identifier if passed the same string
_archiveType = Archive::registerArchiveType(archiveType);
}
private:
int _archiveType;
};
Then, in your implementation ZipArchive
in a method, extract
you can do a static_cast rather than a dynamic one if the int returned archiveType
is the same as the name registered for the zip archive type.
static const char* ZIP_TYPE = "zip";
// specialize your ZipFile making sure
// you pass the correct archive type identifier
// in the constructor
class ZipFile {
public:
ZipFile() : File() {
setArchiveType(ZIP_TYPE);
}
// bla bla
};
void ZipArchive::extract(std::vector<File*> files) {
for (int i = 0; i < files.count(); i++) {
if (files[i]->archiveType() == Archive::registerArchiveType(ZIP_TYPE)) {
ZipFile *zipFile = static_cast<ZipFile*>(files[i]);
// do something with zipFile
}
}
}
source to share
You need to analyze how you will treat Archive
s: do you need to have some general behavior in some places due to undefined types, or not? This will lead to two different designs, so choose carefully.
As said in the comments, you don't seem to need the former.
Let File
is a file descriptor, and ZipFile
, TarFile
- its derivatives. Then, for each type of file, let it Archive
process it.
struct ZipFile
{
File handle;
// zip-specific implementation details
};
struct TarFile
{
File handle;
// tar-specific implementation details
};
class ZipArchive
{
public:
std::vector<ZipFile> list() const;
void extract(std::vector<ZipFile>);
private:
std::vector<ZipFile> archive;
};
And the same for TarArchive
. No more need to handle property rights, pointers, etc .; you also get strong security.
source to share