I haven't used C++ in over a decade, so to learn modern C++ (17) I built a JSON reader based on a pushdown automata (PDA).
The general idea is this:
- A stream is passed to
JsonReader
JsonReader
callsParserStatesManager
, which returns the next state based on the top of the PDA stack, and optionally by reading in an input charJsonReader::ProcessState
then process the returned state - pops/pushes the PDA stack, optionally copies the input char to the currentJValue
, etc- Loop until input is finished or until the error state is reached
Since this is the first time I've touched C++ in a very long time, I'm looking for tips or suggestions on:
- Is this good, clean, idiomatic C++17?
- Is the project layout and class structure what you would expect?
- I have some header-only classes, is that okay?
- Are things named right?
- Is there any code in headers which should be in implementation files, or vice versa?
- Are rvalues/lvalues, references,
const
,auto
, and other C++ features being used correctly? - I wrote Java-esq getters and setters for private members - is this the idiomatic C++ way for accessors/mutators?
- Performance - any issues I've overlooked?
- Any excessive copying or other issues?
- I'm not using pointers anywhere - all memory is either on the stack or managed by STL classes (like vector), is that okay?
And of course, any other suggestions for improvement would be greatly appreciated.
The full project is here - https://github.com/marklar423/jsonreadercpp - and I've added the main relevant files below
main.cc - showing usage
int main(int argc, char *argv[])
{
JsonReader parser;
std::optional<JValue> parsed_json = parser.ParseJsonString(cin);
if (parsed_json.has_value())
PrintJsonTree(parsed_json.value());
else
cout << "No root object in JSON\n";
return 0;
}
JsonReader.h - This is the main class that parses the JSON.
I'm including all the #include
s in this one, as an example of how I use them in the project. #include
s are omitted in subsequent files for brevity.
#ifndef JSONREADERCPP_JSONDESERIALIZER_H_
#define JSONREADERCPP_JSONDESERIALIZER_H_
#include <optional>
#include <iostream>
#include "jsonreadercpp/JValue.h"
#include "jsonreadercpp/statemachine/ParserStatesSymbols.h"
#include "jsonreadercpp/statemachine/ParserStatesManager.h"
#include "jsonreadercpp/statemachine/ParserValueStack.h"
#include "jsonreadercpp/statemachine/ParserMachineStack.h"
namespace jsonreadercpp
{
class JsonReader
{
public:
JsonReader(std::ostream& error_output_stream = std::cerr,
std::ostream& debug_output_stream = std::cout, bool debug_output = false);
std::optional<JValue> ParseJsonString(std::istream& input);
private:
std::ostream& error_output_stream_;
std::ostream& debug_output_stream_;
bool debug_output_;
ParserStatesManager states_manager_;
ParserStatesManager::NextTransition ProcessState(std::istream& input, ParserStateType current_state_type,
ParserValueStack& value_stack, ParserMachineStack& machine_stack);
};
}
#endif
JsonReader.cc
#include "jsonreadercpp/JsonReader.h"
using std::string;
using std::pair;
using std::optional;
namespace jsonreadercpp
{
JsonReader::JsonReader(std::ostream& error_output_stream /*= std::cerr*/,
std::ostream& debug_output_stream /*= std::cout*/, bool debug_output /*= false*/)
: states_manager_(),
error_output_stream_(error_output_stream),
debug_output_stream_(debug_output_stream), debug_output_(debug_output)
{
}
optional<JValue> JsonReader::ParseJsonString(std::istream& input)
{
auto current_state_type = ParserStateType::Start;
ParserValueStack value_stack;
ParserMachineStack machine_stack;
char processed_char = '\0';
bool finished_input = false, has_error = false;
int line_num = 1, char_num = 0, line_char_start = 0;
input >> std::noskipws;
while (!finished_input && !has_error)
{
if (debug_output_)
debug_output_stream_ << '[' << char_num << ']' << processed_char << " => " << ParserStateTypeName(current_state_type) << "\n";
if (current_state_type == ParserStateType::Error)
{
error_output_stream_ << "Error: Unexpected \"" << processed_char << "\""
<< " at Line:" << line_num << ", Line Position:" << (char_num - line_char_start)
<< ", Global Position:" << char_num << "\n";
has_error = true;
}
else
{
auto next_transition_container = ProcessState(input, current_state_type, value_stack, machine_stack);
processed_char = next_transition_container.processed_char;
current_state_type = next_transition_container.transition.next_state;
finished_input = next_transition_container.finished_input;
if (processed_char != '\0') char_num++;
if (processed_char == '\n')
{
line_num++;
line_char_start = char_num;
}
}
}
optional<JValue> result;
if (!has_error)
{
if (value_stack.GetSize() > 1)
{
error_output_stream_ << "Error: Unexpected end of input, JSON isn't complete\n";
}
else
{
result = std::move(value_stack.RemoveRootValue());
}
}
return result;
}
ParserStatesManager::NextTransition JsonReader::ProcessState(std::istream& input, ParserStateType current_state_type,
ParserValueStack& value_stack, ParserMachineStack& machine_stack)
{
//get next state
auto stack_symbol = (machine_stack.GetSize() > 0) ? machine_stack.GetTop() : ParserStackSymbol::None;
auto next_transition_container = states_manager_.GetNextTransition(current_state_type, input, stack_symbol);
const auto& next_transition = next_transition_container.transition;
if (!next_transition_container.finished_input)
{
//stack actions
machine_stack.PopPush(next_transition.stack_pop, next_transition.stack_push);
//input actions
value_stack.AccumulateInput(next_transition_container.processed_char, next_transition.char_destination, current_state_type);
//JValue actions
if (next_transition.value_action == ParserValueAction::Push || next_transition.value_action == ParserValueAction::PushPop)
{
value_stack.PushJValue(next_transition.value_push_type.value());
}
if (next_transition.value_action == ParserValueAction::Pop || next_transition.value_action == ParserValueAction::PushPop)
{
value_stack.PopJValue();
}
}
return next_transition_container;
}
}
JValue.h
namespace jsonreadercpp
{
enum class JValueType
{
String,
Number,
Boolean,
Null,
Object,
Array
};
class JValue
{
public:
//create an object, array, or null
JValue(JValueType value_type) : JValue(value_type, {}) {}
//create a scalar value
JValue(std::string value) : JValue(JValueType::String, {value}) {}
JValue(double value) : JValue(JValueType::Number, {value}) {}
JValue(bool value) : JValue(JValueType::Boolean, {value}) {}
JValue(JValue&& other) = default;
JValue& operator=(JValue&& other) = default;
JValue Clone() const { return JValue(*this); }
std::optional<std::string> GetStringValue() const;
std::optional<double> GetNumberValue() const;
std::optional<bool> GetBooleanValue() const;
std::string GetName() const { return name_; }
JValueType GetValueType() const { return value_type_; }
size_t GetNumberOfChildren() const { return children_.size(); }
const auto& GetChildren() const { return children_; }
bool HasProperty(const std::string& name) { return children_name_indexes_.find(name) != children_name_indexes_.end(); }
//returns false if this parent object is not an array
//takes an rvalue since JValue can only be moved (or Clone()d and then moved)
bool AddArrayChild(JValue&& value);
//returns true if no property with this name already exists, false otherwise
//takes an rvalue since JValue can only be moved (or Clone()d and then moved)
bool AddObjectChild(std::string name, JValue&& value);
//returns true if the element exists, false otherwise
bool RemoveChild(size_t index);
//returns true if the element exists, false otherwise
bool RemoveChild(const std::string& name);
//get the nth child element, either object property or array item
const JValue& operator[](size_t index) const { return children_.at(index); }
//get an object property by name
const JValue& operator[](const std::string& name) const { return this[children_name_indexes_.at(name)]; }
const auto begin() const { return children_.begin(); }
const auto end() const { return children_.end(); }
private:
JValue(JValueType value_type, std::variant<std::string, double, bool> value);
//a copy is recursive and copies all children; it's very expensive
//force the user to use the clone() method so that it doesn't happen by accident
JValue(const JValue& other);
JValue& operator=(const JValue& other);
std::vector<JValue> CopyChildren(const JValue& other);
std::string name_;
JValueType value_type_;
std::vector<JValue> children_;
std::unordered_map<std::string, size_t> children_name_indexes_;
std::variant<std::string, double, bool> value_;
};
}
JValue.cc
namespace jsonreadercpp
{
JValue::JValue(JValueType value_type,
std::variant<std::string, double, bool> value)
: name_(""),
value_type_(value_type),
children_(),
children_name_indexes_(),
value_(value)
{
}
JValue::JValue(const JValue& other)
: name_(other.name_),
value_type_(other.value_type_),
children_(std::move(CopyChildren(other))),
children_name_indexes_(other.children_name_indexes_),
value_(other.value_)
{
}
JValue& JValue::operator=(const JValue& other)
{
name_ = other.name_;
value_type_ = other.value_type_;
children_ = std::move(CopyChildren(other));
children_name_indexes_ = other.children_name_indexes_;
value_ = other.value_;
}
std::vector<JValue> JValue::CopyChildren(const JValue& other)
{
std::vector<JValue> copy;
copy.reserve(other.children_.size());
for (const auto& child : other.children_)
{
copy.emplace_back(child.Clone());
}
return copy;
}
std::optional<string> JValue::GetStringValue() const
{
switch (this->value_type_)
{
case JValueType::String: return {std::get<string>(this->value_) };
case JValueType::Number: return {std::to_string(std::get<double>(this->value_))};
case JValueType::Boolean: return {std::get<bool>(this->value_) ? "true" : "false"};
case JValueType::Null: return {""};
default: return {};
}
}
std::optional<double> JValue::GetNumberValue() const
{
if (this->value_type_ == JValueType::Number)
return { std::get<double>(this->value_) };
return {};
}
std::optional<bool> JValue::GetBooleanValue() const
{
if (this->value_type_ == JValueType::Boolean)
return { std::get<bool>(this->value_) };
return {};
}
bool JValue::AddArrayChild(JValue&& value)
{
bool success = false;
if (this->value_type_ == JValueType::Array)
{
success = true;
//move() here is superfluous, but leaving it just in case `value` changes to a regular value in the future
this->children_.emplace_back(std::move(value));
}
return success;
}
bool JValue::AddObjectChild(std::string name, JValue&& value)
{
bool success = false;
if (this->value_type_ == JValueType::Object && name.length() > 0 && !this->HasProperty(name))
{
success = true;
value.name_ = name;
//move() here is superfluous, but leaving it just in case `value` changes to a regular value in the future
this->children_.emplace_back(std::move(value));
this->children_name_indexes_[name] = this->children_.size() - 1;
}
return success;
}
bool JValue::RemoveChild(size_t index)
{
bool exists = false;
if (index < this->children_.size())
{
exists = true;
string child_name = this->children_[index].name_;
this->children_.erase(this->children_.begin() + index);
if (this->children_name_indexes_.find(child_name) != this->children_name_indexes_.end())
this->children_name_indexes_.erase(child_name);
}
return exists;
}
bool JValue::RemoveChild(const string& name)
{
bool exists = false;
auto kvp = this->children_name_indexes_.find(name);
if (kvp != this->children_name_indexes_.end())
{
exists = true;
this->RemoveChild(kvp->second);
this->children_name_indexes_.erase(name);
}
return exists;
}
}
statemachine/ParserMachineStack.h
namespace jsonreadercpp
{
class ParserMachineStack
{
public:
void PopPush(ParserStackSymbol stack_pop, ParserStackSymbol stack_push);
ParserStackSymbol GetTop() const { return state_machine_stack_.top(); }
size_t GetSize() const { return state_machine_stack_.size(); }
private:
std::stack<ParserStackSymbol> state_machine_stack_;
};
}
statemachine/ParserMachineStack.cc
namespace jsonreadercpp
{
void ParserMachineStack::PopPush(ParserStackSymbol stack_pop, ParserStackSymbol stack_push)
{
if (stack_pop != stack_push)
{
if (stack_pop != ParserStackSymbol::None)
this->state_machine_stack_.pop();
if (stack_push != ParserStackSymbol::None)
this->state_machine_stack_.push(stack_push);
}
}
}
statemachine/ParserState.h
namespace jsonreadercpp
{
class ParserState
{
public:
using TransitionLookup = std::unordered_map<ParserInputSymbol, std::unordered_map<ParserStackSymbol, ParserStateTransition>>;
ParserState(ParserStateType type,
std::initializer_list<ParserStateTransition> transitions = {},
ParserStateTransition else_transition = {ParserInputSymbol::None, ParserStateType::Error})
: state_type_(type), transitions_(), else_transition_(else_transition)
{
for (auto& transition : transitions)
{
this->AddTransition(transition);
}
}
ParserStateType GetStateType() const { return state_type_; }
const TransitionLookup& GetTransitions() const { return transitions_; };
const ParserStateTransition& GetElseTransition() const { return else_transition_; };
bool HasTransition(ParserInputSymbol input_symbol, ParserStackSymbol stack_symbol) const;
const ParserStateTransition& GetTransition(ParserInputSymbol input_symbol, ParserStackSymbol stack_symbol) const
{
return transitions_.at(input_symbol).at(stack_symbol);
}
const ParserStateTransition& GetTransitionOrElse(ParserInputSymbol input_symbol, ParserStackSymbol stack_symbol) const
{
return HasTransition(input_symbol, stack_symbol) ?
GetTransition(input_symbol, stack_symbol) : else_transition_;
}
void AddTransition(ParserStateTransition transition) { transitions_[transition.input].emplace(transition.stack_pop, transition); }
private:
ParserStateType state_type_;
TransitionLookup transitions_;
ParserStateTransition else_transition_;
};
}
statemachine/ParserState.cc
namespace jsonreadercpp
{
bool ParserState::HasTransition(ParserInputSymbol input_symbol, ParserStackSymbol stack_symbol) const
{
bool found = false;
auto find_input = this->transitions_.find(input_symbol);
if (find_input != this->transitions_.end())
{
auto& stack_map = find_input->second;
auto find_stack = stack_map.find(stack_symbol);
found = (find_stack != stack_map.end());
}
return found;
}
}
statemachine/ParserStatesManager.h
namespace jsonreadercpp
{
class ParserStatesManager
{
public:
struct NextTransition
{
const ParserStateTransition& transition;
bool finished_input;
char processed_char;
NextTransition(const ParserStateTransition& transition, bool finished_input = false, char processed_char = '\0')
: transition(transition), finished_input(finished_input), processed_char(processed_char) {}
};
ParserStatesManager() : states_(jsonreadercpp::states::CreateStatesMap()) {}
NextTransition GetNextTransition(ParserStateType current_state_type, std::istream& input, ParserStackSymbol stack_top) const;
private:
std::unordered_map<ParserStateType, ParserState> states_;
};
}
statemachine/ParserStatesManager.cc
namespace jsonreadercpp
{
ParserStatesManager::NextTransition ParserStatesManager::GetNextTransition(ParserStateType current_state_type, std::istream& input, ParserStackSymbol stack_top) const
{
//order of operations: None, None -> * | None, X -> * | X, None -> * | X, Y -> *
const auto& current_state_iter = states_.find(current_state_type);
if (current_state_iter == states_.end())
{
std::cerr << "Unable to find state " << ParserStateTypeName(current_state_type) << "\n";
exit(1);
}
else
{
const auto& current_state = current_state_iter->second;
if (current_state.HasTransition(ParserInputSymbol::None, ParserStackSymbol::None))
{
//None, None -> *
return { current_state.GetTransition(ParserInputSymbol::None, ParserStackSymbol::None) };
}
else if (stack_top != ParserStackSymbol::None && current_state.HasTransition(ParserInputSymbol::None, stack_top))
{
//None, X -> *
return { current_state.GetTransition(ParserInputSymbol::None, stack_top) };
}
else
{
char c = '\0';
if (input >> c)
{
ParserInputSymbol input_symbol = jsonreadercpp::CharToInputSymbol(c);
//X, None -> *
if (current_state.HasTransition(input_symbol, ParserStackSymbol::None))
return { current_state.GetTransition(input_symbol, ParserStackSymbol::None), false, c };
//X, Y -> *
else if (current_state.HasTransition(input_symbol, stack_top))
return { current_state.GetTransition(input_symbol, stack_top), false, c };
else
return { current_state.GetElseTransition(), false, c };
}
else
{
//no more input to read
//there should be no more states, but we need to return something, sooo
return { current_state.GetElseTransition(), true, '\0' };
}
}
}
}
}
statemachine/ParserStateTransition.h
namespace jsonreadercpp
{
struct ParserStateTransition
{
ParserInputSymbol input;
ParserStateType next_state;
ParserStackSymbol stack_pop;
ParserStackSymbol stack_push;
ParserCharDestination char_destination;
ParserValueAction value_action;
std::optional<JValueType> value_push_type;
ParserStateTransition(ParserInputSymbol input, ParserStateType next_state)
: input(input), next_state(next_state),
stack_pop(ParserStackSymbol::None), stack_push(ParserStackSymbol::None),
char_destination(ParserCharDestination::None),
value_action(ParserValueAction::None), value_push_type()
{
}
ParserStateTransition& SetStack(ParserStackSymbol stack_pop, ParserStackSymbol stack_push)
{ this->stack_pop = stack_pop; this->stack_push = stack_push; return *this; }
ParserStateTransition& SetCharDestination(ParserCharDestination char_destination)
{ this->char_destination = char_destination; return *this; }
ParserStateTransition& SetValueAction(ParserValueAction value_action, std::optional<JValueType> value_push_type)
{ this->value_action = value_action; this->value_push_type = value_push_type; return *this; }
};
}
statemachine/ParserValueStack.h
namespace jsonreadercpp
{
class ParserValueStack
{
public:
void AccumulateInput(char input_char, ParserCharDestination destination, ParserStateType current_state_type);
void PushJValue(JValueType type);
void PopJValue();
std::optional<JValue> RemoveRootValue();
size_t GetSize() const { return value_stack_.size(); }
private:
std::stack<std::pair<std::string, JValue>> value_stack_;
std::stringstream property_name_;
std::stringstream scalar_value_;
//collected unicode digits
wchar_t unicode_code_point_ = 0;
ParserCharDestination unicode_destination_;
char ProcessEscapeCharInput(char input_char, ParserCharDestination destination, ParserStateType current_state_type);
//Translate a character into the corresponding escape char,
//i.e. 'n' to '\n', 't' to '\t', etc
char TranslateEscapeChar(char escaped);
//Collects hex codes into unicode_code_point_,
//in the order it appears in the JSON string (big endian)
void CollectUnicodeCodePoint(char input_char);
std::string TranslatUnicodeCodePoint();
};
}
statemachine/ParserValueStack.cc
namespace jsonreadercpp
{
void ParserValueStack::AccumulateInput(char input_char, ParserCharDestination destination, ParserStateType current_state_type)
{
input_char = ProcessEscapeCharInput(input_char, destination, current_state_type);
if (input_char != '\0')
{
if (destination == ParserCharDestination::Name)
this->property_name_ << input_char;
else if (destination == ParserCharDestination::Value)
this->scalar_value_ << input_char;
}
}
void ParserValueStack::PushJValue(JValueType type)
{
optional<JValue> new_value;
if (type == JValueType::Array || type == JValueType::Object || type == JValueType::Null)
{
new_value.emplace(type);
}
else
{
string accumulated_chars = this->scalar_value_.str();
if (type == JValueType::String)
new_value.emplace(accumulated_chars);
else if (type == JValueType::Number)
new_value.emplace(std::stod(accumulated_chars));
else if (type == JValueType::Boolean)
new_value.emplace(accumulated_chars == "true");
}
//add the new value to the top of the stack
this->value_stack_.emplace(this->property_name_.str(), std::move(new_value.value()));
//clear the accumulated values
this->property_name_.str("");
this->scalar_value_.str("");
}
void ParserValueStack::PopJValue()
{
if (this->value_stack_.size() > 1) //root value?
{
pair<string, JValue> top_value(std::move(this->value_stack_.top()));
this->value_stack_.pop();
auto& parent_pair = this->value_stack_.top();
auto& parent_value = parent_pair.second;
if (parent_value.GetValueType() == JValueType::Array)
{
parent_value.AddArrayChild(std::move(top_value.second));
}
else if (parent_value.GetValueType() == JValueType::Object)
{
parent_value.AddObjectChild(std::move(top_value.first), std::move(top_value.second));
}
}
}
optional<JValue> ParserValueStack::RemoveRootValue()
{
optional<JValue> result;
if (value_stack_.size() == 1)
{
result.emplace(std::move(value_stack_.top().second));
value_stack_.pop();
}
return result;
}
char ParserValueStack::ProcessEscapeCharInput(char input_char, ParserCharDestination destination, ParserStateType current_state_type)
{
if (current_state_type == ParserStateType::EscapeChar)
{
input_char = (input_char == 'u') ? '\0' : TranslateEscapeChar(input_char);
}
else if (current_state_type == ParserStateType::UnicodeValue || current_state_type == ParserStateType::UnicodeProperty)
{
//collect unicode code point for later
this->CollectUnicodeCodePoint(input_char);
input_char = '\0';
this->unicode_destination_ = destination;
}
else if (this->unicode_code_point_ > 0)
{
//we have a previously collected unicode code point, save it now
if (this->unicode_destination_ == ParserCharDestination::Name)
this->property_name_ << TranslatUnicodeCodePoint();
else if (this->unicode_destination_ == ParserCharDestination::Value)
this->scalar_value_ << TranslatUnicodeCodePoint();
this->unicode_code_point_ = 0;
}
return input_char;
}
char ParserValueStack::TranslateEscapeChar(char escaped)
{
switch (escaped)
{
case 'b': return '\b';
case 'f': return '\f';
case 'n': return '\n';
case 'r': return '\r';
case 't': return '\t';
}
return escaped;
}
void ParserValueStack::CollectUnicodeCodePoint(char input_char)
{
std::stringstream ss;
ss << input_char;
//convert the hex char to a number
unsigned char hex_num;
ss >> std::hex >> hex_num;
//each hex digit represents 1/2 a byte, so shift by 4
this->unicode_code_point_ = (this->unicode_code_point_ << 4) | (hex_num & 0x0F);
}
std::string ParserValueStack::TranslatUnicodeCodePoint()
{
//reset the conversion state
std::wctomb(nullptr, 0);
std::string utf_chars(MB_CUR_MAX, '\0');
int num_bytes = std::wctomb(&utf_chars[0], unicode_code_point_);
return utf_chars;
}
}
jsonreadercpp::states::CreateStatesMap()
is defined as:
unordered_map<ParserStateType, ParserState> CreateStatesMap()
{
ParserState states[] {
CreateStartState(),
CreateFinishState(),
CreateRootObjectState(),
CreateObjectState(),
<etc>
};
unordered_map<ParserStateType, ParserState> states_map;
for (auto& state : states)
{
auto state_type = state.GetStateType();
states_map.emplace(state_type, std::move(state));
}
return states_map;
}
and the CreatexxxState functions are all basically the same, here's a sample:
ParserState CreateObjectState()
{
return ParserState(ParserStateType::Object,
{
{ ParserInputSymbol::Whitespace, ParserStateType::Object },
ParserStateTransition(ParserInputSymbol::CloseBrace, ParserStateType::PostObject)
.SetStack(ParserStackSymbol::Object, ParserStackSymbol::None)
.SetValueAction(ParserValueAction::Pop, {}),
{ ParserInputSymbol::DoubleQuote, ParserStateType::PropertyString }
},
{ ParserInputSymbol::None, ParserStateType::Error } );
}
Thanks!