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_count
override
functions from Qt may be camel-case:timerEvent(...)
- Constants and macros are all-caps with underscores:
FIND_MIN
constexpr
variables 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
.h
and 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
...
#endif
Makes 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 available
Especially 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 file
Though 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_SIGNAL
s and Q_SLOT
s.
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.git
You 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
master
branch on theupstream/master
git merge upstream master
orgit rebase upstream master
- If all goes well you should see a "fast-forwarding" message, which means that
your local copy of
master
is smoothly updated with the recent changes to the upstream branch - You should also do
git push
, which will updatemaster
on 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_stuff
You 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_stuff
You 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.