Many students used inconsistent indentation and spacing, which made their programs unnecessarily hard to read.
For example, the inconsistent formatting in this function doesn’t do it any favors:
int sum_ab(int a, int b){
if (b < a){
return 0;
}
if (a == b) {
return b;
}
else{
return a + sum_ab(a+1,b);
}
}
Coding style matters in real life: poorly formatted code is hard to read and understand, and so often has more errors than well-formatted code.
Better would be this:
int sum_ab(int a, int b){
if (b < a)
{
return 0;
}
if (a == b)
{
return b;
}
else
{
return a + sum_ab(a+1,b);
}
}
Or this:
int sum_ab(int a, int b){
if (b < a) {
return 0;
}
if (a == b) {
return b;
} else {
return a + sum_ab(a+1,b);
}
}
Or even this:
int sum_ab(int a, int b){
if (b < a) return 0;
if (a == b) return b;
return a + sum_ab(a + 1, b);
}
Don’t use | or & in boolean expressions (e.g. for loops or if- statements). Those operate on bits, and don’t always work the same way as || and && (which is what you should use).
Use check_pre or eassert to check that the pre-conditions of functions are satisfied. You are not required to do this, but it is a wise idea as it is a simple way to catch errors.
Some people used if-statements to check pre-conditions, and then returned some sort of error code if they are not satisfied, e.g.:
// Pre: n >= 0
// Post: returns (not prints!) the sum
// 0 + 1 + 2 + ... + n
int sum(int n) {
if (n < 0) {
return 0; // wrong; or at least just as right as
} // returning -34893
// ...
}
While it’s okay to use an if-statement, it is not okay to return an error code unless the post- condition explicitly says that an error code might be returned.
Instead, invalid pre-conditions should call the error() function, e.g.:
// Pre: n >= 0
// Post: returns (not prints!) the sum
// 0 + 1 + 2 + ... + n
int sum(int n) {
check_pre(n >= 0); // good: throws an exception if n < 0
// ...
}
The error() function throws — not returns — an error object out of the function and ends the program.
Many people implemented the partition function correctly but very inefficiently (even ignoring the requirement that recursion be used). We haven’t been talking seriously about performance yet, but you should always keep it in mind and avoid using “expensive” functions whenever possible.
Here’s an example of a needlessly inefficient function (it also has some style issues, but we’ll ignore those here):
bool is_all_pos(const vector<int>& v) {
if (v.size() == 0) { return true;}
else if (v.at((v.size()-1)) < 0) {return false;}
else {
vector<int> a = v;
a.pop_back();
return (is_all_pos(a));}
}
This function makes lots of copies of the vector v, all of them useless. Instead, in a case like this you should use a helper function and begin/end index variables to keep track of the sub-array currently being processed.
Some people are unaware of some simple shortcuts available in C++, and instead use much more arduous methods. For example, here are two different ways to define a vector:
vector<int> v;
v.push_back(1);
v.push_back(-6);
v.push_back(11);
v.push_back(12);
vector<int> w = {1, -6, 11, 12};
The way w is defined is obviously simpler and shorter, and is what you should do. This happens to be a feature of C++11, and some compilers might not support it.
A related issue is printing out a vector. If you are using the std_lib_cmpt225.h header file, then you can do this:
vector<int> v = {6, 8, 7};
vector<string> vs = {"cat", "dog", "mouse"};
cout << v << endl;
cout << vs << endl;
This code prints v and vs in a nice curly-brace style with values separated by commas. There’s no need to use a loop to do this.
Here’s another example of some code that works but is, arguably, not as simple as it could be:
void partition_test_helper( const ::std::vector< int > & src )
{
::std::vector< int > v = src;
partition( v );
bool isPositiveYet = false;
::std::cout << "partitioned vector == { ";
for ( ::std::vector< int >::const_iterator i = v.begin(); i != v.end(); i++ )
{
if ( !isPositiveYet ) isPositiveYet = ( *i > 0 );
if ( isPositiveYet )
{
test( *i > 0 );
}
else
{
test( *i <= 0 );
}
if ( i != v.begin() )
{
::std::cout << ", ";
}
::std::cout << *i;
}
::std::cout << " }\n";
}
There’s no good reason to write ::std::cout instead of just cout. Indeed, it may even be a problem because it prevents the code from using a version of cout specific to CMPT 255 (although no such special cout exists at the moment).
Also, the for-loop uses a iterators, which is a nice way to iterate through a data structure. But the data type of i is big and complex, and can be simplified by using auto:
void partition_test_helper( const vector< int > & src )
{
vector< int > v = src;
partition( v );
bool isPositiveYet = false;
cout << "partitioned vector == { ";
for ( auto i = v.begin(); i != v.end(); i++ )
{
if ( !isPositiveYet ) isPositiveYet = ( *i > 0 );
if ( isPositiveYet )
{
test( *i > 0 );
}
else
{
test( *i <= 0 );
}
if ( i != v.begin() )
{
cout << ", ";
}
cout << *i;
}
cout << " }\n";
}
The statement auto i = v.begin() declares i to be the data type v.begin() returns. In iterator for-loops like this, auto is almost always the preferred technique because the exact data type of i is usually long and of no value within the loop.
Many people did nowhere near enough testing: often, people would test one case, which is never going to be enough. While determining the exact number of test cases for a function is hard to say in general, you usually want, at least, every line of code to be executed in your function. Any unexecuted lines might have a bug.
Some rules of thumb for testing are: