It is expected that you have basic C++, STL, and Object-Oriented knowledge before diving into Minotaur. Wherever you may lack, Google and Stack Overflow are your friends (so are your fellow members!).
I recommend you a quick read of the Style Guide and then look over carefully the Git Workflow.
Consistent coding style across Minotaur is essential for a readable and understandable codebase. We don't follow a strict style but these are basic guidelines for coding in Minotaur.
- Types begin capitalized and follow camel-case:
RowLineHighlighter,QSlider- Generic types are snake-case:
point,rect,unordered_map
- Generic types are snake-case:
- Functions and variables are snake-case:
reorder_path(),row_countoverridefunctions from Qt may be camel-case:timerEvent(...)
- Constants and macros are all-caps with underscores:
FIND_MINconstexprvariables follow normal variable naming
- Template type parameters are capitalized and camel-case:
typename Element- None-type or generic template parameters are snake-case:
int block_size
- None-type or generic template parameters are snake-case:
- Files names are all lower-case, no underscores:
simulatorwindow.cpp - Header files use
.hand source files use.cpp
Tabs are for losers. Also mixing tabs and spaces gives Git indigestion.
Private class members should be prefixed with m_,
class Rectangle {
public:
Rectangle(int width, int height);
private:
int m_width;
int m_height;
};Unless you want to indicate a non-public type,
namespace det_ {
struct __private_helper_type { ... };
}nullptr is more portable and nicer on the eyes.
Single and multi-line comments should always begin with //, be followed with a space, and be capitalized:
// Add the x and y components. Periods should only be used for multi-line comments,
// This is a multi-line comment inside a block
// of code, which uses double slashes.Block comments with /* */ should be avoided inside code. Javadoc-style comments are encouraged for functions and classes,
/**
* Element-wise addition of vectors.
*
* @param a first vector to add
* @param b second vector to add
* @param res vector to store result
* @param size number of elements
*/
void vector_add(float *a, float *b, float *res, int size);Headers are copy-pasted with the #include directive so including a header with using will pollute the namespace.
Prevents a header from being included twice, causing multiple definitions issues. Most IDEs generate them but
ours should follow the format MINOTAUR_CPP_FILENAME_H. For instance highlighter.h would have
#ifndef MINOTAUR_CPP_HIGHLIGHTER_H
#define MINOTAUR_CPP_HIGHLIGHTER_H
...
#endifMakes code less confusing to read. Inline braces are acceptable for repeatable logic. The open brace
{ should NOT be on a new line,
int function_with_logic(int a, int b, int c) {
if (a > b) { return a - b; }
if (b < c && c < a) { return c + b; }
return a + b + c;
}Lines should be much less than 120 characters long. Continuation indents should be at least four tabs and align to the previous line, and operators should tail indentations,
int function_with_logic(int a, int b, int c) {
if (a + b < c &&
abs(c - a) < abs(a - b) &&
a + b + c < 0) { ... }
}Angle brackets <> should only be used for library or system includes.
// Library includes
#include <string>
#include <cmath>
#include <QSlider>
// Local includes
#include "highligher.h"
#include "../utility/rect.h"It is more performant and cleaner,
class IntBox {
public:
IntBox(int value) :
m_value(value) {}
private:
int m_value;
};Most class definitions are in header files, and we'd like to minimize modifications to those. Instead of
// intbox.h
class IntBox {
public:
IntBox();
private:
int m_value{0}; // or `int m_value = 0;`
};Do this
// intbox.h
class IntBox {
public:
IntBox();
private:
int m_value;
};
// intbox.cpp
#define INTBOX_DEFAULT_VALUE 0
IntBox::IntBox() :
m_value(INTBOX_DEFAULT_VALUE) {}The Standard Template Library uses std::size_t for things to do with size, such as my_vector.size(),
my_map.size(), and is preferred for indexing.
Assert statements are empty when running a release build, so side-effects will not occur.
// different things will occur if running debug or release builds!
void my_function(int x) {
assert(function_with_side_effect(x));
...
}But use them judiciously.
So you can write point1 + point2 instead of point1.add(point2). Arithmetic operators can be overloaded when they
make sense, access operators *, [], and -> should be used for pointer-like types, stream operators << and >>
can be overloaded for log or debug streams, comparison operators for types with order, and so on. More exotic operators
can be overloaded as appropriate. Only overload the comma operator if you're edgy.
Such as
class IntBox {
public:
explicit IntBox(int value);
};To avoid implicit conversion attempts by the compiler. Sometimes implicit conversion operators and constructors are useful, however.
Include directives tell the compiler to essentially copy-paste header files. In C++ this can lead hundreds and even thousands of header files to be loaded into memory and copied per source file.
Instead of including <cstdio> you can include only <iostream>.
If you don't need a class definition, that is, you have only weak references or pointers to MyClass,
use a forward declaration to avoid an additional inclusion (in the header).
// header file
class MyClass;
bool verify_object(MyClass *obj);
// source file
#include "myclass.h"
bool verify_object(MyClass *obj) { ... } // object definition availableEspecially if you need to change them frequently. This can lead to long recompile times in a header file. These include constants, macros, and functions.
For instance, if you want helper functions in your source files. Marking files as static makes them "local" to that
source file and can aid compiler optimizations.
// file1.cpp
static int get_a_value() { ... } // only visible in this file
int get_b_value() { ... } // might be visible in other files
// file2.cpp
extern int get_a_value(); // undefined symbol error
extern int get_b_value(); // can be used in this fileThough avoid usage of extern in Minotaur.
Class member variables are initialized in the order they are declared in the header, regardless of the initialization list. For instance,
class DoubleBox {
public:
DoubleBox(double value) :
m_first(value),
m_second(m_first * 2) {}
private:
double m_second;
double m_first;
};Here, m_second will initialize first, when m_first has not been initialized. The result is that m_second
will have some junk value and m_first will be set to value. In general it is good practice that class members
be initialized in their declaration order.
C++ provides std::unique_ptr and std::shared_ptr that manage dynamically allocated pointers. In most cases
unique_ptr suffices, and a weak reference can be passed around with get(); due to the hierarchical structure
of Qt, it is easy to predict when a pointer is deleted. Use std::make_unique and std::make_shared,
int value = 12;
auto box_uptr = std::make_unique<IntBox>(value);
auto box_sptr = std::make_shared<IntBox>(value);Type deduction can be useful to reduce code bloat in a few instances, but over-usage of auto can make code
difficult to read and debug. auto should be used when casting, making a smart pointer, using an iterator,
or dealing with complex (tuple) types,
auto integer_value = static_cast<int>(12.64);
auto map_it = my_unordered_map.begin();
// instead of
std::unordered_map<int, double>::iterator map_it = ...
auto three_tuple = std::make_tuple(10.5, 10, "hello world");
// instead of
std::tuple<double, int, std::string> three_tuple = ...Never use the basic C-type cast int integer = (int) decimal. Always use one of the specific cast types static_cast,
reinterpret_cast, dynamic_cast, const_cast. A dynamic cast makes use of run-time type info,
and returns nullptr if the cast is invalid.
Lambda expressions are supported in C++11, Minotaur's standard, but they will not be allowed in Minotaur's code-base. Use a functor instead,
struct functor_type {
bool operator()(int arg) const { ... }
};To avoid unnecessary copying. Consider the case,
class ListHolder {
...
public:
std::vector<int> get_list() { return m_list; }
};
...
std::vector<int> my_list = holder.get_list();The function get_list() will return a copy of m_list, then list = holder.get_list() will make another
copy of that list into my_list. Instead, we can do
std::vector<int> &get_list() { return m_list; }
...
std::vector<int> &my_list = holder.get_list();This way my_list is a reference to the list in holder, so no copying is done. If you don't want the list to be
modified, return const std::vector<int> & instead.
The same is for function arguments,
int append_and_sum(int v, std::vector<int> list) {
list.push_back(v);
...
return sum;
}
...
int list_sum = append_and_sum(5, my_list);Here, my_list is copied into the function before the summing occurs. Passing by reference,
int append_and_sum(int v, std::vector<int> &list) removes the copy operation, but side effects
will now occur on list.
Consider the function which creates a returns list,
std::vector<int> make_consecutive_list(std::size_t max) {
std::vector<int> list(max);
for (std::size_t i = 0; i < max; ++i) { list[i] = i; }
return list;
}
...
std:vector<int> filled_list = make_consecutive_list(128);In C++11 no copying or destruction of the list is done; the return value is "moved" to filled_list using a move constructor:
template<typename U> vector(vector<U> &&list). This is a bit nicer than having the caller first allocate the list as in
void make_consecutive_list(std::size_t max, std::vector<int> &list).
STL containers typically have emplace or similar functions which make code less
verbose but may also avoid a copy operation,
struct ListItem {
ListItem(int first, double second, std::string third) { ... }
};
...
std::vector<ListItem> list;
list.emplace_back(4, 76.33, "hello");Qt containers like QList don't have these operations.
The Qt framework tends to be rather massive and in-depth. Many Qt features overlap with STL, such as QList.
This section provides some basic tips on Qt.
Files ending with .ui are XML files that describe a QWidget. These can be edited manually or opened with a
GUI editor in Qt Creator. When compiling a Qt project, the Qt Meta-Object Compiler (MOC) reads these XML files
and generates a ui_*.h file. For instance, ui_imageviewer.h which is used with imageviewer.h. These headers
should only be included in the source files.
// imageviewer.h
#ifndef MINOTAUR_CPP_IMAGEVIEWER_H
#define MINOTAUR_CPP_IMAGEVIEWER_H
#include <QWidget>
namespace Ui {
class ImageViewer;
}
class ImageViewer : public QWidget {
Q_OBJECT
public:
...
private:
Ui::ImageViewer *m_ui;
};
#endif
// imageviewer.cpp
#include "imageviewer.h"
#include "ui_imageviewer.h"
ImageViewer::ImageViewer(QWidget *parent) :
QWidget(parent),
m_ui(new Ui::ImageViewer) { // usable because ui_imageviewer.h is included
m_ui->setup(this);
}The MOC will also generate a moc_imageviewer.cpp based on imageviewer.h which controls the Q_SIGNALs and Q_SLOTs.
In Minotaur we avoid usage of SIGNAL(...) and SLOT(...). Instead do,
connect(pButton, &QPushButton::clicked, pWidget, &MyWidget::clicked_slot);You can connect signals to signals if you need to bounce them around the widget tree,
connect(pButton, &QPushButton::clicked, pParent, &ParentWidget::clicked_signal);
connect(pParent, &ParentWidget::clicked_signal, pChild, &ChildWidget::click_slot);If there is one available. Qt default constructors will have a nullptr parent, so we should
try to pass one if there is one available.
Signals and slots are used by Qt with its event and thread management system. Signals and slots are recommended for dealing with events or communicating across threads by allowing Qt to handle synchronization. But you don't need a series of signals and slots if you're only working in one thread.
STL containers are typically more optimized and are better handled by the compiler. Keep in mind that some Qt functions will return Qt containers. Also, signals and slots that use STL containers will need to have the type registered,
// somewhere in a header file
Q_DECLARE_METATYPE(std::vector<int>);
// at the start of main()
qRegisterMetaType<std::vector<int>>();We recommend using an IDE because it can help detect errors. CLion is free for students and works well with Minotaur, but others work as well. Here is a list of errors that IDEs usually don't detect.
IDEs usually don't detect constructors called from emplacement operations, like container emplace functions, or
std::make_unique. If you get a deluge of template errors from a make_unique double check that the parameters
passed match the constructor of the type.
Qt tends to issue forward declarations of its classes. A frequent example are its event types: QMouseEvent,
QTimerEvent, and QPaintEvent. If you include <QBasicTimer> you will get a forward declaration of QTimerEvent
and will need to include <QTimerEvent> explicitly to have the whole definition.
You might get very confusing error messages when using a smart pointer on an incomplete type that itself has a smart pointer to an incomplete type. Consider the case
// MyClassA.h
class IncompleteType;
class MyClassA {
// ...
std::unique_ptr<IncompleteType> m_val;
};
// MyClassB.h
class MyClassA; // type is incomplete
class MyClassB {
// ...
std::unique_ptr<MyClassA> m_a;
};In compiling MyClassB.cpp you might get an incomplete type error to IncompleteType. This is because
the unique_ptr deleter instantiates the compiler-generated destructor of MyClassA, which needs
to know the complete definition of IncompleteType. Since explicitly define a destructor ~MyClassA
and set it as = default in MyClassA.cpp.
Git tends to be a fine art and it takes some experience to avoid strange merge or rebasing issues. This section will outline some Git best practices.
You should always work on a fork of the main minotaur-cpp repository. To set up your local workspace, click fork on the repo main page and clone your fork. This sets up your fork as the remote named origin. You'll then want to add the main
repo as a remote called upstream. This might look like
git clone https://github.com/mogball/minotaur-cpp.git
cd minotaur-cpp
git remote add upstream https://github.com/uwnrg/minotaur-cpp.gitYou should now have one branch, master, which Git will try to synchronize with the master branch on your fork. You should
periodically update your local master branch with the upstream master. You can "watch" the main repo for any updates to
the upstream master branch. To update your local master branch,
- Fetch the upstream repo with
git fetch upstream
- This updates a local copy of the upstream repo
- Merge or rebase your
masterbranch on theupstream/master
git merge upstream masterorgit rebase upstream master
- If all goes well you should see a "fast-forwarding" message, which means that
your local copy of
masteris smoothly updated with the recent changes to the upstream branch - You should also do
git push, which will updatemasteron your fork, in case you need to start over
If you accidentally made local commits to your master branch (i.e. you forgot to create
a new branch for changes) either you will have merge conflicts, a merge commit from git merge,
or replaying your changes message from git rebase. In this case your local branch will
diverge from the upstream master, which will only make things worse.
- If you have merge conflicts, abort the merge first
git merge --abort - Make a copy of your modified master
git checkout -b master_copy - Delete your modified master
git branch -D master - Check out a fresh copy of master from upstream
git checkout master upstream/master - Point your branch back to your fork
git branch master -u origin/master
Since pull requests are always squashed when merged, make sure you keep your
master updated with upstream.
When you make a new feature, first ensure that your local master is up-to-date with
the upstream repo. Then, checkout a new branch hopefully with the name format <your_name>/<feature_or_fix>
git fetch upstream
git rebase upstream/master
git checkout -b jeff/fixing_stuffYou will make all your changes on this branch. Whenever you feel you have reached a milestone, or may
need to back up your code, you can add your changes and commit them. You should get into the habit of
running git status to see which changes have not been staged, and which have not been committed, to avoid
adding or committing accidental changes.
# Check for unexpected changes
git status
# All good! Stage all changes
git add .
# Commit staged changes
git commit -m "my commit message"Commit messages should briefly describe the changes you have made. Frequent and meaningful commits, even if they are one-liners, are good.
If you see in git status that you made changes to a file you don't recognize or made by accident (you can
double check by running git diff, you can erase unstaged changes (you will lose these changes!) run
git checkout -- folder/file_to_reset.c. To erase all unstage changes from root run git checkout -- . and
to unstage changes you added do git reset HEAD folder/file_to_unstage.c.
Once you are satisfied with your commit(s), push them to your fork,
git push -u origin jeff/fixing_stuffYou only need to specify the upstream master with -u the first time.
If you forget the names of your branches, git branch will list local
branches that are checked out and indicate which you are on.
Once you have pushed your new commits, you can create a Pull Request on Github. Usually an option to do so from your recently updated branch will appear on the main repo site, or you can go to your fork and click "New Pull Request". PRs will show up on the main repo and will show the changes and additions you've made. Usually someone will review your PR, suggesting changes. Additional commits pushed to your fork feature branch will automatically be updated on the PR. Once your changes are approved, they will be squashed and merged. No additional changes should be added to this branch.
You should then go and update your master git checkout master && git pull upstream master and delete your
local feature branch git branch -D jeff/fixing_stuff. You can keep the branches on your fork for history,
but I recommend keeping branches to a minimum locally.
Usually we try to work in different files, and coordinate when we modify the same files so that features and fixes aren't lost in a merge conflict resolution process. If you are resolving merge conflicts in a file that has recently been modified, double check to make sure you aren't overriding important features.
Merge conflicts usually arise when comparing a pull request with master. Simple ones can be resolved
through the Github GUI but more complicated ones need to be resolved through the command line. If conflicts
are too difficult to resolve you can try merging with a previous commit. If you know the recent few commits to
master had lots of changes to a file you are working on, use git log to see the commit history. Grab the commit
hash, looks something like 044d8cd4a4d44e7a307f20999f38daa16f812d3e, then you can git merge <commit_hash> or
git rebase <commit_hash>. Before moving onto the more difficult merges.
Larger pull requests may not be squashed into one commit and can instead be squashed by the
PR author into several commits and rebased onto master. If you want to squash the last 10 commits
into 3 commits, run git rebase -i HEAD~10. Make sure you leave the top commit as pick.
Choose 2 other commits to leave as pick, and commits below them changed to squash will be
squashed into the pick commit above.
If you have merged commits in your PR things will get messy, because you will need to re-resolve
all conflicts, and the commit history and git diff will be messed up. More surgical methods
are needed if we still want to squash. (It is recommended to rebase to keep up-to-date with master).
Sometimes Git goes wrong. Here are some things you can do.
If you need to break apart commits to move around, git reset HEAD~ will reset and unstage changes of
the most recent commit. You can then git add and git commit to separate the file changes into different
commits.
If you've accidentally squashed into master and need to recover your commits, git reflog will show you
commits that have been lost on the tree. Hopefully they show up there. Make a new copy of master and git cherry-pick
these commits in order onto that branch, using the commit hashes.
If a lost commit doesn't show up in git reflog more advanced disaster recovery is available. Check out
the Git documentation.