Template error messages may be notorious, but are by no means always long and unreadable. In this case, the entire error message (from gcc) is:
test.cpp: In function ‘void dosomething(T&) [with T = X]’:
test.cpp:11: instantiated from here
test.cpp:6: error: no match for ‘operator+=’ in ‘x += 5’
As in your Python example, you get a "stack trace" of template instantiation points, and a clear error message indicating the problem.
Sometimes, template-related error messages can get much longer, for various reasons:
- The "stack trace" might be much deeper
- The type names might be much longer, as templates are instantiated with other template instantiations as their arguments, and displayed with all their namespace qualifiers
- When overload resolution fails, the error message might contain a list of candidate overloads (which might each contain some very long type names)
- The same error may be reported many times, if an invalid template is instantiated in many places
The main difference from Python is the static type system, leading to the necessity of including the (sometimes long) type names in the error message. Without them, it would sometimes be very difficult to diagnose why the overload resolution failed. With them, your challenge is no longer to guess where the problem is, but to decipher the hieroglyphics that tell you where it is.
Also, checking at runtime means that the program will stop on the first error it encounters, only displaying a single message. A compiler might display all the errors it encounters, until it gives up; at least in C++, it should not stop on the first error in the file, since that may be a consequence of a later error.
Ditch the OOP
The member functions probably don’t need to be. You can have the Parser
class provide a few primitive operations (accept character, backtrack, &c.) and implement the rest of the parser as free functions taking a Parser
reference. They can live in the source file (under an anonymous namespace) thus keeping the header nice and minimal.
In this sense, Parser
is really a minimal wrapper around a stream state.
Also, it helps to factor out combinators such as many
, choice
, and so on, to avoid writing error-prone and verbose manual loops.
Sketch
In Parse.h
, your entire public API.
unique_ptr<const Program> parse(istream&);
Your parser state can be entirely private to Parse.cpp
.
struct Parser {
Parser(istream& stream) : stream(stream) {}
bool accept(const char expected) {
char actual;
if (!stream.get(actual)) return false;
if (expected == actual) return true;
stream.seekg(-1, ios::cur);
return false;
}
template<class F, class O>
bool accept_if(F predicate, O output) {
char actual;
if (!stream.get(actual)) return false;
if (predicate(actual)) {
*output++ = actual;
return true;
}
return false;
}
void expect(const char expected) { if (!accept(expected)) throw ...; }
void push_mark() { marks.push_back(stream.tellg()); }
void pop_mark() { stream.seekg(marks.back()); drop_mark(); }
void drop_mark() { marks.pop_back(); }
private:
istream& stream;
vector<istream::pos_type> marks;
};
The public API implementation just forwards to the start production of a grammar with a new parser.
unique_ptr<const Program> parse(istream& stream) {
return parse_program(Parser(stream));
}
Output iterators are a fairly convenient way to produce multiple results.
template<class F, class O>
bool many_if(Parser& parser, F predicate, O output) {
bool success;
while (parser.accept_if(predicate, output)) success = true;
return success;
}
Pointers have the advantage of a conversion to bool.
unique_ptr<const Expression> number(Parser& parser) {
string digits;
auto append = back_inserter(digits);
return many_if(isdigit, append) ? make_unique<Number>(digits) : nullptr;
}
And so on. The real strengths of C++ to leverage when writing parsers are:
- Generic programming
- Iterators
- Streams
Objects, not so much.
Best Answer
The difference between
long long
andint int
is thatlong
modifies a type, rather than being a type itself.long
is really a shorthand forlong int
andlong long
a shorthand forlong long int
.More specifically
int
is a type specifier, just likechar
orbool
.long
is a type modifier. Other type modifiers areunsigned
andsigned
andshort
.If one of the modifiers is missing then the type will fall back to a default. E.g. if there is no
signed
orunsigned
then the type will be signed. If there is noshort
orlong
the default size depends on the compiler and the architecture.Take a look at this table on Wikipedia for a full list of how different type specifiers and modifiers can be combined.
Edit:
In current versions of the C and C++ standards
long
andshort
are actually type specifiers in their own right. This doesn't change the way that things can be combined though.