I have implemented a simple class for reading values from command line arguments. It looks like this:
#pragma once
#include <string>
#include <cstring>
#include <stdexcept>
//! Reads key value pairs, or just the flag for booleans
class CommandLine
{
public:
//! Use the ignore argument for ignoring first N command line arguments, useful if the first argument is for example a file name
CommandLine(int argc, const char** argv, int ignore = 1) : _argc(argc), _argv(argv), _ignore(ignore) {}
//! Finds a command line parameter using given string. The param is searched as is, so if your params begin with -, you need to call this method with "-myParam"
//! The found param will be set to true if value was found and false otherwise
//! Parse errors will not throw, instead you will get a default value and found will be false
template <typename T>
T getValue(const std::string& flagName, T defaultValue, bool& found);
//! Shorthand if you don't care if value is found
template <typename T>
T getValue(const std::string& flagName, T defaultValue) { bool unused; return getValue(flagName, defaultValue, unused); }
//! Parses value at index, this is absolute index from 0, the ignore parameter in constructor does not affect this
//! Otherwise behaves as the std::string overload
template <typename T>
T getValue(int offset, T defaultValue, bool& found);
//! Shorthand if you don't care if value is found
template <typename T>
T getValue(int offset, T defaultValue) { bool unused; return getValue(offset, defaultValue, unused); }
private:
int _argc;
const char** _argv;
int _ignore = 0;
int findFlag(const std::string& name) const;
};
template <>
int CommandLine::getValue(int offset, int defaultValue, bool& found)
{
try
{
int res = std::stoi(_argv[offset]);
found = true;
return res;
}
catch (std::invalid_argument const&) {}
catch (std::out_of_range const&) {}
found = false;
return defaultValue;
}
template <>
bool CommandLine::getValue(const std::string& flagName, bool defaultValue, bool& found)
{
return findFlag(flagName) != -1;
}
template<typename T>
inline T CommandLine::getValue(const std::string& flagName, T defaultValue, bool& found)
{
int i = findFlag(flagName);
if (i > -1 && i+1 < _argc)
{
return getValue(i + 1, defaultValue, found);
}
else
{
found = false;
return defaultValue;
}
}
template <typename T>
T CommandLine::getValue(int offset, T defaultValue, bool& found)
{
static_assert(false, "Parsing of this type is not supported.");
}
int CommandLine::findFlag(const std::string& name) const
{
for (int i = _ignore; i < _argc; ++i)
{
if (name == _argv[i])
{
return i;
}
}
return -1;
}
You can see that what I did is that I have template specialization for int for the method getValue(int offset, T defaultValue, bool& found), and I can later add others like double. My intent was to throw a readable error if one tries to parse value that does not have a parser implemented, so I added a generic implementation that should only be used if no specialization is available. From code above:
template <typename T>
T CommandLine::getValue(int offset, T defaultValue, bool& found)
{
static_assert(false, "Parsing of this type is not supported.");
}
I assumed this code would be only attempted to be compiled if you try to use it. This assumption seems correct for MSVC. But when I tried it on linux with g++ (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0, I get an error:
myname@linuxpc:~/programming/project/build$ g++ -std=c++17 -c ../src/main.cpp
In file included from ../src/main.cpp:5:
../src/helpers/CommandLine.h: In member function ‘T CommandLine::getValue(int, T, bool&)’:
../src/helpers/CommandLine.h:70:17: error: static assertion failed: Parsing of this type is not supported.
70 | static_assert(false, "Parsing of this type is not supported.");
| ^~~~~
^~~~~
I am thinking it is more likely that I am doing this wrong.
How to implement a class that has explicit template specializations for each type and throws a compile error if nonexistent specialization is attempted to be called?