I've noticed that whenever a shader program class is implemented (from some questions on this site, as well as more reputable implementations), that:
The class tries to do too much. Binding to the current context, setting attribute variable values, and much more.
Use the JavaBeans pattern in one form or another, allowing the shader program to be in an invalid or incomplete state. For example, in between
addShaderFromSource
calls, the program is only partially built, and not usable.
My ideal shader program class would do much less; its purpose would just be to manage the lifetime of a shader program object, while maintaining a valid state throughout the object's lifetime (either a zero program or a valid, linked program), and the solution I came up with to achieve this uses the builder pattern.
void deleteAttachedShaders(GLuint programId) {
constexpr GLsizei MAX_SHADER_COUNT = 5;
GLuint shaderIds[MAX_SHADER_COUNT];
GLsizei shaderActualCount;
glGetAttachedShaders(programId, MAX_SHADER_COUNT, &shaderActualCount, shaderIds);
for(int i = 0; i < shaderActualCount; ++i) {
glDetachShader(programId, shaderIds[i]);
glDeleteShader(shaderIds[i]);
}
}
class GLShaderProgram
{
public:
class Builder {
public:
Builder() : linked_(false) {
programId_ = glCreateProgram();
}
Builder &addShaderFromSource(std::string const &shaderSource, GLenum shaderType) {
char const *shaderCStr = shaderSource.c_str();
GLuint shaderId = glCreateShader(shaderType);
glShaderSource(shaderId, 1, &shaderCStr, nullptr);
glCompileShader(shaderId);
glAttachShader(programId_, shaderId);
return *this;
}
Builder &addShaderFromFile(std::string const &shaderFile, GLenum shaderType) {
std::ifstream fileName(shaderFile);
std::istreambuf_iterator<char> fileBegin(fileName), fileEnd;
std::string fileContents(fileBegin, fileEnd);
return addShaderFromSource(fileContents, shaderType);
}
GLShaderProgram linkProgram() {
glLinkProgram(programId_);
GLShaderProgram theProgram(programId_);
deleteAttachedShaders(programId_);
linked_ = true;
return theProgram;
}
~Builder() {
if(!linked_) {
glDeleteProgram(programId_);
}
}
private:
GLuint programId_;
bool linked_;
};
GLShaderProgram() : programId_(0) { }
GLShaderProgram(GLShaderProgram &&other) {
*this = std::move(other);
}
GLShaderProgram &operator=(GLShaderProgram &&other)
{
programId_ = other.programId_;
other.programId_ = 0;
if (other.programId_ != 0) {
glDeleteProgram(other.programId_);
}
return *this;
}
~GLShaderProgram() {
glDeleteShader(programId_);
}
GLuint programId() const { return programId_; }
GLShaderProgram(GLShaderProgram const &other) = delete;
GLShaderProgram &operator=(GLShaderProgram const &other) = delete;
private:
GLShaderProgram(GLuint programId) : programId_(programId) { }
GLuint programId_;
};
Example usage:
auto shaderProgram = GLShaderProgram::Builder()
.addShaderFromFile("vertex.glsl", GL_VERTEX_SHADER)
.addShaderFromFile("fragment.glsl", GL_FRAGMENT_SHADER)
.linkProgram();
glUseProgram(shaderProgram.programId());
Any comments on the code's safety/quality would be appreciated.