Writing Stupid Code

A surprisingly good idea

In this course, I am going to encourage you to write "stupid code" rather than "clever code" or "enterprise-class" code.

Definition

Stupid Code is code that you will be able to understand and work with three years from now when you are scrambling to fix a bug; even at 3am before a submission deadline; and even on a borrowed laptop.

The Principles of Stupid Code

Stupid code tends to follow a few basic principles:

Locality

Locality is your most powerful tool for writing stupid code. When you need to understand code, it is very useful to have all of the code you need to understand in one place, clearly arranged.

Thankfully, C++ has some great features that can help you achieve locality.

Scopes

The most powerful feature for working with locality in C++ is scoping. Whenever you see { int x; ... } you know that whatever is within the brackets is going to be a unit of functionality *and* that whatever the variable x is doing, it isn't going to be used outside of that chunk.

Also, if you use the RAII ("resource allocation is initialization") paradigm -- and I encourage you to do so -- scopes show when certain resources are active.

	{ //draw badges
		UseProgram use(ui_tex_prog);
		BindTexture bind0(GL_TEXTURE0, badges_tex);

		GLuint attrib_Position = ui_tex_prog("Position");
		GLuint attrib_TexCoord = ui_tex_prog("TexCoord");

		badge_verts.VertexAttribPointer(attrib_Position);
		badge_texcoords->VertexAttribPointer(attrib_TexCoord);

		EnableVertexAttribArray enable_position(attrib_Position);
		EnableVertexAttribArray enable_texcoord(attrib_TexCoord);

		ui_tex_prog["scale"] = BigRad;
		ui_tex_prog["tint_color"] = tint;

		ui_tex_prog["translate"] = glm::vec2(col_mid, badge_y);
		glDrawArrays(GL_TRIANGLE_STRIP, 0, badge_verts.count);
	}

Get in the habit of throwing a comment in the first line of a scope to make it clear what's going on.

Let me call out RAII once more. Very useful for making it so you never forget to write clean-up code.

struct GLTexture {
	GLTexture() { glGenTextures(1, &tex); }
	~GLTexture() { glDeleteTextures(1, &tex); tex = 0; }
	GLuint tex = 0;
}

Lambdas

Lambdas (local functions) in C++ give you a great way to encapsulate and re-use local scopes without breaking the flow of the code.

Quick lambda refresher:

	auto func = [/*capture*/](/*params*/){
		/*body*/
	};

Example: you just finished writing this nice interpolation function:

void Title::update(float elapsed) {
	t += elapsed; //accumulate time
	//fade in the main title:
	if (t < 0.5f) {
		main_color.a = 0.0f;
	} else if (t < 1.5f) {
		main_color.a = (t - 0.5f) / (1.5f - 0.5f);
	} else {
		main_color.a = 1.0f;
	}
}

And now you want to add a subtitle:

void Title::update(float elapsed) {
	t += elapsed; //accumulate time
	auto fade = [&t](float t0, float t1, float *alpha) {
		if (t < t0) {
			*alpha = 0.0f;
		} else if (t < t1) {
			*alpha = (t - t0) / (t1 - t0);
		} else {
			*alpha = 1.0f;
		}
	};
	//fade in titles:
	fade(0.5f, 1.5f, &main_color.a);
	fade(0.8f, 1.8f, &sub_color.a);
}

Example: say you want to sort some data by a particular key:

struct Player {
	glm::vec2 position;
	float score;
};
std::vector< Player > players;

//sort players by score:
std::sort( players.begin(), players.end(), [](Player const &a, Player const &b) {
	return a.score < b.score;
});

Inline Structures, Initializers

Two different features that go together well.

Group together related variables in a clean way (also great when debugging):

struct {
	float speed = 1.0f;
	std::string title = "racer-x";
} config;

Great for temporary transformations of data:

struct Range {
	Range(uint32_t index_, int32_t min_, int32_t max_) : index(index_), min(min_), max(max_) { }
	uint32_t index;
	int32_t min, max;
	bool marked = false;
};
std::vector< Range > ranges;
for (auto const &sprite : sprites) {
	ranges.emplace_back(&sprite - &sprites[0], sprite.x - sprite.r, sprite.x + sprite.r);
}
//...

Static variables

In many cases -- especially when working with OpenGL -- you want a resource that is initialized once, but putting it at the global scope is wrong for a number of reasons. The static keyword helps with this.

void draw() {
	//really bad (leaks buffer):
	GLuint buffer = 0;
	if (buffer == 0) {
		/*initialize buffer*/
	}
	//fine (but not a great habit, for subtle reasons):
	static GLuint buffer = 0;
	if (buffer == 0) {
		/*initialize buffer*/
	}
	//superb:
	static GLuint buffer = []() -> GLuint {
		GLuint buffer = 0;
		/*initialize buffer*/
		return buffer;
	}();
	//...
}

Note that static variables used in this way tend to work against failing early (see below), so there is a balance to be struck here!

Paranoia

Paranoia -- in the coding sense -- means stating what you know clearly. C++ gives you some great ways to do this: types, assert, and static_assert.

Types

Variable types are an easy way to help understand a program. Seeing Image i; vs int i; is going to be very informative.

Basic tips for type sanity:

Assert

Never Write An Assert Than Should Fail

i.e. "don't use assert() to check data errors, use it to check programming errors"

(For data errors, I recommend throwing std::runtime_error or -- if you are feeling oldschool -- exit(1).)

//bad assert: [bad data file can cause assert()]
	std::vector< glm::vec3 > normals = load_normals_from_file();
	for (auto const &n : normals) {
		assert(n != glm::vec3(0.0f, 0.0f, 0.0f) && "normals shouldn't be zero-length");
	}
//good assert: [loading routine establishes invariant; assert() checks it later]
	std::vector< glm::vec3 > normals = load_normals_from_file();
	for (auto const &n : normals) {
		if (n == glm::vec3(0.0f, 0.0f, 0.0f)) {
			std::cerr << "ERROR loading file: found a zero-length normal." << std::endl;
			return;
		}
	}
	/* ... code that preserves non-zero normals goes here */
	for (auto const &n : normals) {
		assert(n != glm::vec3(0.0f, 0.0f, 0.0f) && "normals can't be zero-length or file loading wouldn't have succeeded");
	}

Note that this is a fuzzy line. You will need to make judgement calls as to whether, e.g., errors in the contents of data files written by your code are covered by assert(). You probably won't actually care in these fuzzy cases; so just pick an option.

Asserts Are Comments You Can Trust

Consider making them more helpful by including a && "description of problem" in the parameter. This will get printed when the assert fails, which can jump-start your debugging process.

At the very least, place a comment near the assert justifying why it will never fail.

assert(pos[i].x <= pos[i+1].x && "pos array is always sorted in x-coordinate.");
//or:
assert(pos[i].x <= pos[i+1].x); //pos array is always sorted in x-coordinate.

Other Important Stuff

Static Assert

A special assert that checks at compile time, rather than runtime. Very useful when, e.g., making sure that structures are packed.

struct Color {
	uint8_t r,g,b,a;
};
static_assert(sizeof(Color) == 4, "Color is packed as expected.");

Fail Early

With all this talk of runtime errors, it's worth thinking for a moment about *when* you want runtime errors to trigger. That is, ideally, as soon as possible.

This comes up especially with resource loading. Consider a missing texture in the final cutscene of the game. You want to find out about this ASAP rather than after playing through the entire experience.

void init() {
	intro_texture = load_texture("intro_texture.png");
	level_texture = load_texture("level_texture.png");
	final_texture = load_texture("final_texture.png");
}
void draw_final_scene() {
	draw_texture(final_texture);
}

The Load template in our base code (2022 version linked here) can help with this:

Load< Mesh > mesh(LoadTagDefault, []() -> const Mesh * {
	return new Mesh(data_path("meshes/main.mesh"));
});

Of course, this breaks when you start having more assets than you have memory to work with; in which case you may still wish to have some notion of a streaming asset handle and a special mode that makes sure that all handles connect to real assets.

Laziness

Do only what needs doing.

Corollary: always write code for an immediate reason.

Corollary: don't optimize until you've profiled.

Corollary: do things the easy way first.

Corollary: prefer to be wrong than to be right in a cumbersome way (i.e., avoid unnecessary generality).

Minimize Dependencies

Pick a compiler for each platform. I recommend:

Compiling with three different tools should -- one hopes -- give one three different sets of static checks, ensuring better code quality.

Pick a way to call that compiler on each platform. You'll want something explicit in its build steps and easy to remember how to run:

The Standard Library

The standard library in C++ is really, really good. It will save you a lot of work.

Things you should make sure you are aware of (and should use as needed):

Other Notes

Use #pragma once in headers instead of #ifdef wrapping.

Check out if constexpr. Great for making compile-time decisions.

Understand the cleverness of if (T *t = weak_ptr_to_t.lock()).

Stick to a coding style. I prefer:

Remember that using tab characters for indentation reaffirms the intrinsic goodness and diversity of humanity. So just do it.