3
\$\begingroup\$

I am currently developing a C++ OpenGL game engine to help myself learn computer graphics and game design later on. However, this project is becoming really big, and I am unsure if I am making the correct design choices with the code. Parts of the code are also becoming harder to add to, especially the parts that deal with UI.

Please be really harsh on it, as I really want to learn what I should be doing when making a program this big.

Link: https://github.com/Kirugaming/ProjectBahamut2/tree/map-geometry

Here is some code files that you could review:

Engine.cpp

//
// Created by kirut on 10/16/2023.
//

#include "Engine.h"
#include "backends/imgui_impl_sdl2.h"
#include "backends/imgui_impl_opengl3.h"


Engine::Engine(Project &chosenProject) : project(chosenProject), windowSize(displayMode.h-50, (displayMode.w) != 0) {
    SDL_GetCurrentDisplayMode(0, &displayMode);
    if (initRendering(
            (int) windowSize.height,
            (int) windowSize.width)) {
        std::cout << "ENGINE_ERROR::INIT_RENDERING" << std::endl;
    }

    baseShader = new Shader();



    // init game
    game.camera = Camera(glm::vec3(0.0f, 0.0f, 2.0f));
    game.level = new Level();

    // init engine ui

    glViewport(windowSize.height / 6, windowSize.width / 6, windowSize.height / 1.5, windowSize.width / 1.5);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    glEnable(GL_DEBUG_OUTPUT);

}

/*
 * Startup SDL rendering and OpenGL rendering
 */
int Engine::initRendering(int winHeight, int winWidth) {
    // Create Window
    window = SDL_CreateWindow("ProjectBahamut", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, winWidth, winHeight,
                              SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL | SDL_WINDOW_MAXIMIZED);

    // opengl
    SDL_GL_LoadLibrary(nullptr);
    SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 4 );
    SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 6 );
    SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE );
    glContext = SDL_GL_CreateContext(window);
    // Init Glad
    if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
        std::cout << "Glad failed to Initialize!" << std::endl;
        return -1;
    }

    return 0;
}

void Engine::engineLoop() {
    while(!game.quit){
        game.computeDeltaTime();

        // Events
        SDL_PumpEvents();
        eventMonitor(); // query SDL events
        KeyboardInput();

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
        glClearColor(.2f, .3f, .3f, 1.0f);

        baseShader->use();

        for (Brush *brush: game.level->brushList) {
            drawMeshSubClass(brush);
        }

        drawGameObjects(game.level->gameObjects);
        baseShader->unUse();

        ui->renderUI(&game);

        SDL_GL_SwapWindow(window);
    }
}


void Engine::eventMonitor() {
    while( SDL_PollEvent( &event ) ) {
        ImGui_ImplSDL2_ProcessEvent(&event); // Forward your event to backend
        switch (event.type) {
            case SDL_QUIT:
                game.quit = true;
                break;
            case SDL_WINDOWEVENT:
                switch (event.window.event) {
                    case SDL_WINDOWEVENT_RESIZED:
                        windowSize.height = event.window.data1;
                        windowSize.width = event.window.data2;
                        break;
                }
                break;
        }
    }
}

Engine::~Engine() {
    // cleanup
    SDL_GL_DeleteContext(glContext);
    SDL_DestroyWindow( window );
    SDL_Quit();
}

void Engine::KeyboardInput() { // possible to do this elsewhere but its here for now
    if (inputManager.getKeyDown("w")) {
        game.camera.movement(Camera::FORWARD, game.deltaTime);
    }
    if (inputManager.getKeyDown("s")) {
        game.camera.movement(Camera::BACKWARD, game.deltaTime);
    }
    if (inputManager.getKeyDown("a")) {
        game.camera.movement(Camera::LEFT, game.deltaTime);
    }
    if (inputManager.getKeyDown("d")) {
        game.camera.movement(Camera::RIGHT, game.deltaTime);
    }
    if (inputManager.getKeyDown("up")) {
        game.camera.setPitch(1);
    }
    if (inputManager.getKeyDown("down")) {
        game.camera.setPitch(-1);
    }
    if (inputManager.getKeyDown("left")) {
        game.camera.setYaw(-1);
    }
    if (inputManager.getKeyDown("right")) {
        game.camera.setYaw(1);
    }
    if (inputManager.getKeyDown("tab")) {
        if (isWireframeModeEnabled) {
            isWireframeModeEnabled = false;
        } else {
            isWireframeModeEnabled = true;
        }
    }
}

void Engine::drawGameObjects(const std::vector<GameObject*>& gameObjects) const {
    for (GameObject* model : gameObjects) {
        for (Script *script : model->scripts) {
            script->run();
        }

        baseShader->editShaderWithMat4("view", game.camera.getView());
        baseShader->editShaderWithMat4("perspective", glm::perspective(glm::radians(45.0f), 1.88791f, 0.1f, 100.0f));
        model->draw(*baseShader);

        drawGameObjects(model->nestedGameObjects);
    }
}

void Engine::drawMeshSubClass(Mesh *mesh) {
    baseShader->editShaderWithMat4("view", game.camera.getView());
    baseShader->editShaderWithMat4("perspective", glm::perspective(glm::radians(45.0f), 1.88791f, 0.1f, 100.0f));
    mesh->draw(*baseShader);

}

Engine.h

//
// Created by kirut on 10/16/2023.
//

#ifndef PROJECTBAHAMUT_ENGINE_H
#define PROJECTBAHAMUT_ENGINE_H



#include <vector>
#include <chrono>
#include "Camera.h"
#include "GameObject.h"
#include "SDL.h"
#include "imgui.h"
#include "Level.h"
#include "engineUI.h"
#include "Game.h"
#include "Projects.h"
#include "InputManager.h"
#include "Brush.h"

class engineUI;

static struct WindowSize {
    int height;
    int width;
} windowSize;

class Engine {
public:
    SDL_DisplayMode displayMode;
    SDL_Window *window = nullptr;
    SDL_GLContext glContext = nullptr;
    SDL_Event event;


    WindowSize windowSize;
    Project project;
    Game game;
    bool isWireframeModeEnabled = false;

    InputManager &inputManager = InputManager::getInstance();
    engineUI *ui;


    // Dear Imgui to be implemented
    Engine(Project &chosenProject);
    ~Engine();


    void engineLoop();



private:
    Shader *baseShader; // make this so its made after opengl context is created

    int initRendering(int winHeight, int winWidth); // SDL and OpenGL
    void eventMonitor();
    void KeyboardInput();
    void drawMeshSubClass(Mesh *mesh);
    void drawGameObjects(const std::vector<GameObject*>& gameObjects) const;
};


#endif //PROJECTBAHAMUT_ENGINE_H

engineUI.cpp

//
// Created by kirut on 10/20/2023.
//

#include "engineUI.h"
#include "Script.h"


engineUI::engineUI(SDL_Window *window, SDL_GLContext &glContext) {

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();

    // Setup Platform/Renderer backends
    ImGui_ImplSDL2_InitForOpenGL(window, glContext);
    ImGui_ImplOpenGL3_Init();

    // setup top down brush viewer





    loadUiIcons();
}

engineUI::~engineUI() {
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplSDL2_Shutdown();
    ImGui::DestroyContext();
}

void engineUI::renderUI(Game *game) {

    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplSDL2_NewFrame();
    ImGui::NewFrame();

    projectFileExplorer();

    configureNextWindowPosSize(ImVec2(ImGui::GetIO().DisplaySize.x - 320, 0),
                               ImVec2(320, 300));

    ImGui::Begin("Game Object Editor", reinterpret_cast<bool *>(true), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar);

    ImGui::Text("Game Object Editor");
    ImGui::SameLine();
    if (ImGui::Button("Save Map")) {
        engine->game.level->save();
    }

    ImGui::Separator();

    if (ImGui::TreeNode("Map Geometry")) {
        for (Brush *brush : game->level->brushList) {
            if (brush->name.empty()) {
                if (ImGui::Button("##")) {
                    selectedObject = nullptr;
                    this->selectedBrush = brush;
                }
            } else {
                if (ImGui::Button(brush->name.c_str())) {
                    selectedObject = nullptr;
                    selectedBrush = brush;
                }
            }
        }
        ImGui::TreePop();
    }
    if (ImGui::BeginPopupContextWindow()) {
        if (ImGui::MenuItem("Add Cube Brush")) {
            game->level->brushList.push_back(new Brush());
        }
        ImGui::EndPopup();
    }
    if (ImGui::TreeNode("Game Objects")) {
        drawGameObjectButton(game->level->gameObjects);
        ImGui::TreePop();
    }




    if (selectedObject != nullptr) {

        objectEditWindow(selectedObject);
    }
    if (selectedBrush != nullptr) {

        brushEditWindow(selectedBrush);
    }

    ImGui::End();

    ImGui::ShowDemoWindow();


    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}

void engineUI::objectEditWindow(GameObject *gameObject) {
    configureNextWindowPosSize(ImVec2(ImGui::GetIO().DisplaySize.x - 320, 300),
                               ImVec2(320, 300));
    ImGui::Begin("Object Editor", reinterpret_cast<bool *>(true), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize);
    ImGui::Text("This is the object editor window!");

    gameObject->name = drawTextInput("Object Name", gameObject->name);

    drawVec3Input("Object Position", gameObject->transform.position);
    drawVec3Input("Object Rotation", gameObject->transform.rotation);
    drawVec3Input("Object Scale", gameObject->transform.scale);

    ImGui::Separator();
    ImGui::Text("Attached Scripts:");
    for (int i = 0; i < gameObject->scripts.size(); ++i) {
        ImGui::Text(gameObject->scripts[i]->path.filename().string().c_str());
        ImGui::SameLine();
        if (ImGui::Button("Delete")) {
            gameObject->scripts.erase(gameObject->scripts.begin() + i);
        }
    }



    ImGui::Button("Drop New Script Here", ImVec2(320, 50));
    if (ImGui::BeginDragDropTarget()) {
        if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("DRAG_SCRIPT_FILE")) {
            ImGui::SetMouseCursor(ImGuiMouseCursor_NotAllowed);
            ImGui::BeginTooltip();
            ImGui::Text("Drop Script here to add to game object");
            ImGui::EndTooltip();
            gameObject->scripts.push_back(new Script(static_cast<char*>(payload->Data), gameObject)); // turn the void pointer to string :)
        }
        ImGui::EndDragDropTarget();
    }

    ImGui::End();
}

void engineUI::projectFileExplorer() {
    configureNextWindowPosSize(ImVec2(0, 0),
                               ImVec2(320, ImGui::GetIO().DisplaySize.y));
    ImGui::Begin("Project Explorer", reinterpret_cast<bool *>(true), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize);
    ImGui::Text("Project Explorer");
    ImGui::Separator();

    displayFileTree(engine->project.path, 0);


    ImGui::End();
}
/*
 * Recursively goes through files from project root and make button for each
 * if directory then does function again
 */
void engineUI::displayFileTree(const std::string &path, int level) {
    std::string tabs(level, '\t');
    if (level > 0) {
        tabs += "-";
    }

    for (const auto &file: std::filesystem::directory_iterator(path)) {
        auto fileName = file.path().filename().string();
        ImGui::Text(tabs.c_str()); // it would be nice if UNICODE CHARACTERS WORKED
        ImGui::SameLine();

        if (file.is_directory()) {
            // find if directory is open in ui
            bool isOpen = openFolders.find(file.path().string()) != openFolders.end();

            ImGui::Image((void *) (intptr_t) (isOpen ? icons["folderOpen"]->id : icons["folderClosed"]->id),
                         ImVec2(20, 20), ImVec2(0, 1), ImVec2(1, 0));
            ImGui::SameLine();

            if (ImGui::Button(fileName.c_str())) {
                // path is added to open directories if not open
                if (!isOpen) {
                    openFolders.insert(file.path().string());
                } else {
                    openFolders.erase(file.path().string());
                }
            }

            if (isOpen) {
                displayFileTree(file.path().string(), level + 1);
            }
        } else {
            handleFileTypes(file);
        }
    }
}

void engineUI::handleFileTypes(const std::filesystem::directory_entry& file) {
    auto fileName = file.path().filename().string();
    std::string fileType = fileName.substr(fileName.find('.') + 1, fileName.length());

    if (fileType == "lua") { // some files have drag drop and some don't
        if (ImGui::Button(fileName.c_str())) { // do file action
        }
        if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
            ImGui::SetDragDropPayload("DRAG_SCRIPT_FILE", file.path().string().c_str(),
                                      file.path().string().capacity()); // i don't know why capacity works and not size
            ImGui::Text(fileName.c_str());
            ImGui::EndDragDropSource();
        }
    } else { // normal button
        if (ImGui::Button(fileName.c_str())) { // do file action
            if (fileType == "bem") {
                selectedObject = nullptr;
                engine->game.level = new Level(file.path().string());
            }
        }
    }
}

void engineUI::loadUiIcons() {
    icons["folderClosed"] = new Texture(R"(Assets\icons\folder-solid.png)");
    icons["folderOpen"] = new Texture(R"(Assets\icons\folder-open-regular.png)");
    icons["object"] = new Texture(R"(Assets\icons\cube-solid.png)");
}

void engineUI::configureNextWindowPosSize(ImVec2 position, ImVec2 size) {
    ImGui::SetNextWindowPos(position, ImGuiCond_Once);
    ImGui::SetNextWindowSize(size, ImGuiCond_Once);
}

void engineUI::drawVec3Input(const std::string &inputName, glm::vec3 &vector3) {
    ImGui::Separator();
    ImGui::Text((inputName + ":").c_str());
    glm::vec3 vec3Buffer = vector3;
    if (ImGui::DragFloat3(("##" + inputName).c_str(), glm::value_ptr(vector3), 0.005f)) {

    }
}

std::string engineUI::drawTextInput(const std::string &inputName, std::string &text) {
    ImGui::Separator();
    ImGui::Text((inputName + ":").c_str());

    char buffer[256];
    strcpy(buffer, text.c_str());

    if (ImGui::InputText(("##" + inputName).c_str(), buffer, 256)) {
        text = buffer;
    }

    return text;
}

void engineUI::drawGameObjectButton(std::vector<GameObject*> &gameObjects) {
    for (auto& object : gameObjects) {
        ImGui::Image((void*)(intptr_t)icons["object"]->id, ImVec2(20, 20), ImVec2(0, 1), ImVec2(1, 0));
        ImGui::SameLine();
        if (object->name.empty()) {
            if (ImGui::Button("##")) {
                selectedBrush = nullptr;
                this->selectedObject = object;
            }
        } else if (ImGui::TreeNode(object->name.c_str())) {
            this->selectedObject = object;
            selectedBrush = nullptr;
            drawGameObjectButton(selectedObject->nestedGameObjects);
            ImGui::TreePop();
        }
    }
}

void engineUI::brushEditWindow(Brush *brush) {
    configureNextWindowPosSize(ImVec2(ImGui::GetIO().DisplaySize.x - 320, 300),
                               ImVec2(320, 300));

    ImGui::Begin("Object Editor", reinterpret_cast<bool *>(true), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize);
    ImGui::Text("This is the object editor window!");

    brush->name = drawTextInput("Object Name", brush->name);

    ImGui::Separator();
    ImGui::Checkbox("Snapping enabled for this brush", &brush->isSnapEnabled);


    ImGui::Separator();
    if (brush->isSnapEnabled) {
        ImGui::Text("Object Position:");
        if (ImGui::DragFloat3("##Object Position", glm::value_ptr(brush->transform.position), 1.0f)) {
        }
    } else {
        drawVec3Input("Object Position", brush->transform.position); // normal drag
    }
    drawVec3Input("Object Rotation", brush->transform.rotation);

    ImGui::Separator();
    if (brush->isSnapEnabled) {
        ImGui::Text("Object Scale:");
        if (ImGui::DragFloat3("##Object Scale", glm::value_ptr(brush->transform.scale), 1.0f)) {
        }
    } else {
        drawVec3Input("Object Scale", brush->transform.scale); // normal drag
    }

    if (brush->isSnapEnabled) {
        brush->snapToWholeVerts();
    }

    ImGui::End();
}

engineUI.h

//
// Created by kirut on 10/20/2023.
//

#ifndef PROJECTBAHAMUT_ENGINEUI_H
#define PROJECTBAHAMUT_ENGINEUI_H

struct Game;

#include "SDL_video.h"
#include "imgui.h"
#include "backends/imgui_impl_opengl3.h"
#include "backends/imgui_impl_sdl2.h"
#include "Engine.h"
#include <filesystem>
#include <unordered_set>

class Project;
class Engine;




class engineUI {
public:
    Engine *engine;

    engineUI(SDL_Window *window, SDL_GLContext &glContext);
    ~engineUI();

    void renderUI(Game *game);

private:

    std::map<std::string, Texture*> icons;
    GameObject *selectedObject = nullptr;
    Brush *selectedBrush = nullptr;
    std::unordered_set<std::string> openFolders;
    std::string *fileDragTemp;


    void objectEditWindow(GameObject *gameObject);
    void brushEditWindow(Brush *brush);
    void loadUiIcons();
    static void configureNextWindowPosSize(ImVec2 position, ImVec2 size);
    static void drawVec3Input(const std::string& inputName, glm::vec3 &vector3);
    static std::string drawTextInput(const std::string& inputName, std::string &text);
    void drawGameObjectButton(std::vector<GameObject*> &gameObjects);
    void projectFileExplorer();
    void displayFileTree(const std::string &path, int level);
    void handleFileTypes(const std::filesystem::directory_entry& file);

    void openFile(std::string path);
};


#endif //PROJECTBAHAMUT_ENGINEUI_H

also bonus question would be if yaml would be the best way of saving maps to a file like how I am doing in level.cpp

//
// Created by kirut on 10/18/2023.
//

#include "Level.h"

Level::Level() = default;

Level::Level(const std::string& levelFile) {
    path = levelFile;
    try {
        YAML::Node level = YAML::LoadFile(levelFile);

        // go
        for (auto &&object : level["Brushes"]) {
            brushList.push_back(addBrush(object));
        }
        // go through game objects
        for (auto &&object : level["GameObjects"]) {
            gameObjects.push_back(addGameObject(object));
        }

    } catch (const std::exception& e) {
        std::cerr << "Map Loading Error: " << e.what() << std::endl;
    }
}

void Level::save() const {
    std::ofstream file(path);
    file << "# -ProjectBahamut Map File-\n# You can edit the map through this file or the engine\n";

    YAML::Node map;
    for (int i = 0; i < brushList.size(); ++i) {
        map["Brushes"][i]["name"] = brushList[i]->name;
        // position
        map["Brushes"][i]["position"][0] = gameObjects[i]->transform.position.x;
        map["Brushes"][i]["position"][1] = gameObjects[i]->transform.position.y;
        map["Brushes"][i]["position"][2] = gameObjects[i]->transform.position.z;
        // rotation
        glm::vec3 vecRotation = gameObjects[i]->transform.rotation;
        map["Brushes"][i]["rotation"][0] = vecRotation.x;
        map["Brushes"][i]["rotation"][1] = vecRotation.y;
        map["Brushes"][i]["rotation"][2] = vecRotation.z;
        // scale
        map["Brushes"][i]["scale"][0] = gameObjects[i]->transform.scale.x;
        map["Brushes"][i]["scale"][1] = gameObjects[i]->transform.scale.y;
        map["Brushes"][i]["scale"][2] = gameObjects[i]->transform.scale.z;
    }

    for (int i = 0; i < gameObjects.size(); i++) {
        map["GameObjects"][i]["name"] = gameObjects[i]->name;
        map["GameObjects"][i]["model"] = gameObjects[i]->modelPath;
        // position
        map["GameObjects"][i]["position"][0] = gameObjects[i]->transform.position.x;
        map["GameObjects"][i]["position"][1] = gameObjects[i]->transform.position.y;
        map["GameObjects"][i]["position"][2] = gameObjects[i]->transform.position.z;
        // rotation
        glm::vec3 vecRotation = gameObjects[i]->transform.rotation;
        map["GameObjects"][i]["rotation"][0] = vecRotation.x;
        map["GameObjects"][i]["rotation"][1] = vecRotation.y;
        map["GameObjects"][i]["rotation"][2] = vecRotation.z;
        // scale
        map["GameObjects"][i]["scale"][0] = gameObjects[i]->transform.scale.x;
        map["GameObjects"][i]["scale"][1] = gameObjects[i]->transform.scale.y;
        map["GameObjects"][i]["scale"][2] = gameObjects[i]->transform.scale.z;
        // any scripts
        for (int j = 0; j < gameObjects[i]->scripts.size(); ++j) {
            map["GameObjects"][i]["scripts"][j] = gameObjects[i]->scripts[j]->path.string();
        }
    }

    file << map;
    file.close();

}

GameObject* Level::addGameObject(const YAML::Node &object) {
    auto *newGameObject = new GameObject(
            object["name"].as<std::string>(),
            object["model"].as<std::string>(),
            glm::vec3(object["position"][0].as<float>(), object["position"][1].as<float>(), object["position"][2].as<float>()),
            glm::vec3(object["rotation"][0].as<float>(), object["rotation"][1].as<float>(), object["rotation"][2].as<float>()),
            glm::vec3(object["scale"][0].as<float>(), object["scale"][1].as<float>(), object["scale"][2].as<float>())
    );

    for (int i = 0; i < object["scripts"].size(); ++i) {
        newGameObject->scripts.push_back(new Script(object["scripts"][i].as<std::string>(), newGameObject));
    }

//    for (int i = 0; i < object["nestedObjects"].size(); ++i) {
//        newGameObject->nestedGameObjects.push_back(
//                addGameObject(object["nestedObjects"][i]));
//    }

    return newGameObject;
}

Brush *Level::addBrush(const YAML::Node &object) {
    auto *newBrush = new Brush(
            object["name"].as<std::string>(),
                    Transform(glm::vec3(object["position"][0].as<float>(), object["position"][1].as<float>(), object["position"][2].as<float>()),
                              glm::vec3(object["rotation"][0].as<float>(), object["rotation"][1].as<float>(), object["rotation"][2].as<float>()),
                              glm::vec3(object["scale"][0].as<float>(), object["scale"][1].as<float>(), object["scale"][2].as<float>()))
            );
    return newBrush;
}
\$\endgroup\$
1
  • \$\begingroup\$ Thank you for this notice with my post. I have corrected the post to include more parts of my code to review. \$\endgroup\$
    – kiru
    Jan 29 at 17:06

1 Answer 1

1
\$\begingroup\$

This looks like the wrong stream:

        std::cout << "ENGINE_ERROR::INIT_RENDERING" << std::endl;

Errors ought to go to std::cerr. What's even more notable is that the program continues as if nothing had happened. We probably want to abort at that point (probably by throwing an exception).


The header files seem to include many more library headers than necessary. Only include what's needed for the definitions in the header - those that are needed only for the implementation should be included from the implementation files.


I'm concerned at the public members of classes - particularly the pointer members. It would be very harmful for outside code to modify those (e.g. by setting them to null pointers), so they definitely ought to be private.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.