52

The question Where should I put functions that are not related to a class has sparked some debate over whether it makes sense in C++ to combine utility functions in a class or just have them exist as free functions in a namespace.

I come from a C# background where the latter option does not exist and thus naturally trend toward using static classes in the little C++ code I write. The highest voted answer on that question as well as several comments however say that free functions are to be preferred, even suggesting static classes were an anti-pattern. Why is that so in C++? At least on the surface, static methods on a class seem indistinguishable from free functions in a namespace. Why thus the preference for the latter?

Would things be different, if the collection of utility functions needed some shared data, e.g. a cache one could store in a private static field?

  • Sounds a bit like the "functional decomposition" antipattern. – user281377 Feb 11 '12 at 12:18
  • 12
    Short answer: You just don't need a class to wrap these functions. Free functions are a much cleaner fit for your task than crunching them into some pseudo-OO construct, which is a workaround you only need in "purely-OO" languages. – Christian Rau Feb 11 '12 at 16:02

10 Answers 10

52

I guess to answer that we should compare the intentions of both classes and namespaces. According to Wikipedia:

Class

In object-oriented programming, a class is a construct that is used as a blueprint to create instances of itself – referred to as class instances, class objects, instance objects or simply objects. A class defines constituent members which enable these class instances to have state and behavior. Data field members (member variables or instance variables) enable a class object to maintain state. Other kinds of members, especially methods, enable a class object's behavior. Class instances are of the type of the associated class.

Namespace

In general, a namespace is a container that provides context for the identifiers (names, or technical terms, or words) it holds, and allows the disambiguation of homonym identifiers residing in different namespaces.

Now, what are you trying to achieve by putting the functions in a class (statically) or a namespace? I would wager that the definition of a namespace better describes your intention - all you want is a container for your functions. You don't need any of the features described in the class definition. Note that the first words of the class definition are "In object-oriented programming", yet there is nothing object-oriented about a collection of functions.

There are probably technical reasons as well but as someone coming from Java and trying to get my head around the multi-paradigm language that is C++, the most obvious answer to me is: Because we don't need OO to achieve this.

| improve this answer | |
  • 2
    +1 for "we don't need OO to achieve this" – Ixrec Jan 5 '15 at 21:37
  • I agree with this as a no-fan of OO, but I guess that there are some fews situations where using an All-static class is the only solution, by example, replacing a namespace, since you can't declare a nested namespace inside a class. – Wael Boutglay Apr 1 '18 at 20:35
  • How about you need to derive from the all-static-member class to have polymorphic behavior? – Kemin Zhou Jun 26 at 4:27
  • 2
    @KeminZhou Polymorphism only makes sense if you are creating instances of the class doesn't it? And instances only make sense if you have non-static members. – Gyan aka Gary Buyn Aug 9 at 4:54
31

I'd be very cautious in calling that an anti-pattern. Namespaces are usually preferred, but as there are no namespace templates and namespaces can't be passed as template parameters, using classes with nothing but static members is quite common.

| improve this answer | |
  • 3
    The other answers have glossed over the last line of the OP. Namespaces are pretty much named scopes, so if you need access control, classes are the only tool available. – cmannett85 Feb 11 '12 at 11:43
  • 2
    @cbamber85 to be fair, I had put that line in only after reading the first two answers. – PersonalNexus Feb 11 '12 at 12:49
  • @PersonalNexus Ah fair enough, I didn't realise! – cmannett85 Feb 11 '12 at 12:51
  • 10
    @cbamber85: That is not entirely true. You'd get even better encapsulation by putting "private" functions in the implementation file, so the users don't get to see even the private declaration. – UncleBens Feb 11 '12 at 13:21
  • 3
    +1 Templates are indeed a point where namespaces still lack features. – Christian Rau Feb 11 '12 at 16:05
15

Back in the day I needed to take a FORTRAN class. I knew other imperative languages by then, so I figured I could do FORTRAN without much studying. When I turned in my first homework, professor returned it to me and asked to redo it: he said that Pascal programs written in FORTRAN syntax do not count as valid submissions.

Similar issue is in play here: using static classes to host utility functions in C++ is a foreign idiome to C++.

As you mentioned in your question, using static classes for utility functions in C# is a matter of necessity: free-standing functions are simply not an option in C#. The language needed to develop a pattern for allowing programmers define free-standing functions in some other way - namely, within static utility classes. This was a Java trick taken word-for-word: for example, java.lang.Math and System.Math of .NET are nearly isomorphic.

C++, however, offers namespaces, a different facility for achieving the same goal, and it actively uses it in the implementation of its standard library. Adding an extra layer of static classes is not only unnecessary, but also somewhat counterintuitive to readers without C# or Java background. In a sense, you are introducing a "loan translation" into the language for something that can be expressed natively.

When your functions need to share data, the situation is different. Because your functions are no longer unrelated, Singleton pattern becomes the preferred way of addressing this requirement.

| improve this answer | |
  • 8
    +1, except for recommending singletons. They are not preferred in C++! Functions may be in a class if they need to share state, but classes should not be singletons unless they really, really need to be. And they usually don't. – Maxpm Feb 11 '12 at 18:50
  • @Maxpm Don't get me wrong, singleton is preferred only to global static variables. Other than that, there is nothing "preferred" about that. – Sergey Kalinichenko Feb 11 '12 at 19:47
  • 2
    There is this good article, Singletons: Solving Problems You Didn't Know You Never Had Since 1995. Summarized it says that coupling the well known instance to the class itself unnecessarily limits your flexibility and gives you nothing. Most of the time just have a class and have a shared instance somewhere, but don't make it singleton. – Jan Hudec Feb 17 '12 at 9:00
  • I'm not sure "foreign idiom" is the right term. It's a very common idiom. I think quite a few programming teams are using e2 of Stroustrup... – Nick Keighley May 25 '17 at 11:10
12

Perhaps you need to ask why you would want an all-static class?

The only reason I can think of is that other languages (Java and C#) that are very much 'everything is a class' require them. These languages cannot create top-level functions at all, so they invented a trick to keep them, and that was the static member function. They're a bit of a workaround, but C++ doesn't need such a thing, you can create brand new top-level, independent functions directly.

If you need functions that operate on a specific data item, then it makes sense to bundle them into a class that holds the data, but then, these stop being functions and start being members of that class that operate on the class' data.

If you have a function that doesn't operate on a particular data type (I use the word here as classes are ways to define new data types) then it really is an anti-pattern to shove them into a class, for no other than semantic purposes.

| improve this answer | |
  • 10
    Indeed - I would say that the "class with all static members" in Java is actually a hack to get around a limitation of Java. – Charles Salvia Feb 11 '12 at 15:03
  • @CharlesSalvia: I was being polite :) I have the same bad feeling about main() being part of a java class too, though I understand why they did that. – gbjbaanb Feb 12 '12 at 13:39
  • This actually seems to provide the answer to why you'd want an all-static class. Or do I misunderstand you? For example, I need to hold a variable whose value can change that is read by one class (which I intend to store in ROM) and written to by another class (which will be in RAM). Simply placing the at a known ram location is not sufficient - wrapping it (and it's accessors) in a class allows me to fine tune access control. Pretty much what you describe in your second paragraph. – iheanyi Jun 12 '14 at 14:52
6

At least on the surface, static methods on a class seem indistinguishable from free functions in a namespace.

In other words having a class instead of a namespace has no advantages.

Why thus the preference for the latter?

For one thing it saves you typing static all the time, though that's arguably a rather minor benefit.

The main benefit is that it's the least powerful tool for the job. Classes can be used to create objects, they can be used as the type of variables or as template arguments. Neither of those are features you want for your collection of functions. So it's preferable to use a tool that doesn't have those features, so that the class can't accidentally be misused.

Following from that using a namespace makes it also immediately clear to any users of your code that this is a collection of functions and not a blueprint to create objects from.

| improve this answer | |
5

... several comments however say that free functions are to be preferred, even suggesting static classes were an anti-pattern. Why is that so in C++? At least on the surface, static methods on a class seem indistinguishable from free functions in a namespace. Why thus the preference for the latter?

An all-static class will get the job done, but it's like driving a 53-foot semi truck to the grocery store for chips and salsa when a four-door sedan will do (i.e., it's overkill). Classes come with a small amount of additional overhead and their existence might give someone the impression that instantiating one might be a good idea. The free functions offered by C++ (and C, where all functions are free) don't do that; they're just functions and nothing else.

Would things be different, if the collection of utility functions needed some shared data, e.g. a cache one could store in a private static field?

Not really. The original purpose of static in C (and later C++) was to offer persistent storage that's usable at any level of scope:

int counter() {
    static int value = 0;
    value++;
}

You can take the scope out to the level of a file, which makes it visible to all narrower scopes but not outside the file:

static int value = 0;

int up() { value++; }
int down() { value--; }

A private static class member in C++ serves the same purpose but is limited to the scope of a class. The technique used in the counter() example above also works inside C++ methods and is something I'd actually recommend doing if the variable doesn't need to be visible to the entire class.

| improve this answer | |
  • I suppose a group of unrelated utility functions which share no data should be better grouped in a namespace. But if you have a group of tightly coupled utility functions that need access to shared data and perhaps some access control, a static class is the best choice. A static within a function is not reentrant. Using a module global... slightly better in this context, but I still think an all static class is the best solution if you have the requirements I described. – iheanyi Jun 12 '14 at 14:56
  • If they share data might they be better as a class without static methods? – Nick Keighley May 25 '17 at 11:23
  • @NickKeighley That depends on who wins the debate over whether it's better to create one instance and pass it around to everything that needs it or just make it static-in-class. – Blrfl May 25 '17 at 15:20
  • There's more than one "original purpose" of static. Not only is it a storage duration, it is also a linkage – Caleth Aug 13 at 9:43
2

As John Carmack famously said:

"Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function."

I think that pretty much sums it up. Why would you make it forcefully a class if it's clearly not a class? C++ has the luxury that you can actually use functions; in Java, everything is a method. Then you need utility classes, and lots of them.

| improve this answer | |
1

If a function maintains no state and is reentrant, there doesn't seem to be much point in shoving it inside a class, (unless forced to by the language). If the function maintains some state, (eg. it can only be made thread-safe by means of a static mutex), then static methods on a class seem appropriate.

| improve this answer | |
1

Much of the discourse on the topic here makes sense, though there is something very fundamental about C++ that makes namespaces and classes/structs very different.

Static classes (classes where all members are static, and the class will never be instantiated) are themselves objects. They are not simply a namespace to contain functions.

Template meta-programming allows us to use a static class as a compile-time object.

Consider this:

template<typename allocator_type> class allocator
{
public:
    inline static void* allocate(size_t size)
    {
        return allocator_type::template allocate(size);
    }
    inline static void release(void* p)
    {
        allocator_type::template release(p);
    }
};

To use it we need functions contained inside a class. A namespace will not work here. Consider:

class mallocator
{
    inline static void* allocate(size_t size)
    {
        return std::malloc(size);
    }
    inline static void release(void* p)
    {
        return std::free(p);
    }
};

Now to use it:

using my_allocator = allocator<mallocator>;

void* p = my_allocator::allocate(1024);
...
my_allocator::release(p);

So long as a new allocator exposes an allocate and release function that is compatible, switching to a new allocator is easy.

This can not be achieved with namespaces.

Do you always need functions to be part of a class? No.

Is using a static class an anti-pattern? It depends on the context.

Would things be different, if the collection of utility functions needed some shared data, e.g. a cache one could store in a private static field?

In that case what you're trying to achieve is likely to be best served through object-oriented programming.

| improve this answer | |
0

I want to demonstrate why it may not always be an anti-pattern even with OOP design. I'll be using an online tutorial that I have worked on several times.

Sometimes namespaces are preferred for free functions, however, there may be times that you don't want to use a namespace. One reason could be naming collisions or you don't want the user of your library to use using namespace xyz; exposing them to that scope. If you have a faculty of functions that are all related or all work on a single class object then you might want to consider having a non-instantiable class with static methods only.

Take, for example, I have gone through implementing this tutorial a few times to become familiar with the Vulkan API. In my first implementation, I followed it precisely from the site. The author has one superclass that contains just about everything within the program in it besides a couple of global variables, helper structs, and a couple of freestanding functions. There were almost 50 member variables, 25 - 30 functions with nearly 1000 lines of code.

On my second attempt, I started from a clean solution and stayed with the source implementation but I broke it into multiple classes each responsible for their own types having only a few functions each. This gave me some trouble due to pass pointers around, object lifetime, concurrency, synchronization, etc. and how Vulkan handles its procedures.

You have to be explicit with every single thing when writing a Vulkan App since the API's core was written and designed around C bindings with full C++ support, it later started to support other languages, It took some research and troubleshoot on how to pass around objects from one class to another without the Validation Layers triggering some kind of error or the application crashing due to either a segfault or an unhandled exception, etc., but I eventually got it to work.

This time around, I'm still currently working on it. I've decided to take a different approach. I started doing research for this exact concept which led me here as I was just looking for some insight. This attempt at writing a Vulkan APP, I'm doing exactly what was asked of this question. Before I give reasons to why I'm doing it this way, or why it might be better, I'm going to show pseudo-class interfaces from my first two attempts. Then I'll show some of my current implementations and finish with why it may not always be a bad design. Now onto the examples.


Vulkan Tutorial v1

// main.cpp
#include "App.h"

int main() {
    // create an instance of app
    try {
        app.run();
    } catch { exception ) {
        // log message return failure
    }
    return success;
}

// App.h
#pragma once

// include GLFW - Vulkan Header, GLM header
// include stl-libraries vector, string, iostream, etc...

// some const globals values

// some structs

// a couple of freestanding functions, callbacks, functions pointers, and a static function

class App {
public:
    void run() { initWindow(); initVulkan(); mainLoop(); cleanUp(); } 
private:
    // about 30 - 50 member variables
    GLFWwindow* window;
    VkInstance instance;
    VkDebugUtilsMessengerEXT debugMessenger;        
    VkSurfaceKHR surface;
    VkPhysicalDevice physicalDevice;
    VkDevice logicalDevice;
    VkQueue graphicsQueue;
    VkQueue presentQueue;        
    VkSwapchainKHR swapchain;
    VkFormat swapchainImageFormat;
    VkExtent2D swapchainExtent;
    std::vector<VkImage> swapchainImages;
    std::vector<VkImageView> swapchainImageViews;
    std::vector<VkFramebuffer> swapchainFramebuffers;        
    VkRenderPass renderPass;
    VkDescriptorSetLayout descriptorSetLayout;
    VkPipelineLayout pipelineLayout;
    VkPipeline graphicsPipeline; 
    VkCommandPool commandPool;
    VkImage depthImage;
    VkDeviceMemory depthImageMemory;
    VkImageView depthImageView;
    VkImage textureImage;
    VkDeviceMemory textureImageMemory;
    VkImageView textureImageView;
    VkSampler textureSampler;
    std::vector<Vertex> vertices; 
    std::vector<uint32_t> indices;
    VkBuffer vertexBuffer;
    VkDeviceMemory vertexBufferMemory;
    VkBuffer indexBuffer;
    VkDeviceMemory indexBufferMemory;          
    std::vector<VkBuffer> uniformBuffers;
    std::vector<VkDeviceMemory> uniformBuffersMemory;
    vkDescriptorPool descriptorPool;
    std::vector<VkDescriptorSet> descriptorSets;
    std::vector<VkCommandBuffer> commandBuffers; 
    std::vector<VkSemaphore> imageAvaiableSemaphores;
    std::vector<VkSemaphore> renderFinishedSemaphores;
    std::vector<VkFence> inFlightFences;
    std::vector<VkFence> imagesInFlight;
    size_t currentFrame = 0;
    bool framebufferResized = false;

    void initWindow();
    static void framebufferResizeCallback(params); 
    
    void initVulkan() {
        createInstance();
        setupDebugMessenger();
        createSurface();
        pickPhysicalDevice();
        createLogicalDevice();
        createSwapChain();
        createImageViews();
        createRenderPass();
        createDescriptorSetLayout();
        createGraphicsPipeline();
        createCommandPool();
        createDepthResources();
        createFramebuffers();
        createTextureImage();
        createTextureImageView();
        createTextureSampler();
        loadModel();
        createVertexBuffer();
        createIndexBuffer();
        createUniformBuffers();
        createDescriptorPool();
        createDescriptorSets();
        createCommandBuffers();
        createSyncObjects();
    }

    void mainLoop() {
        while(condition) {
            pollEvents();
            drawFrame();
        }
        vkDeviceWaitIdle(device);
    }

    void cleanSwapChain() {
        // destroy certain resources
        // free others
    }

    void cleanup() {
        cleanupSwapchain();
        // destroy everything else in proper order
    }

    void recreateSwapchain() { .... }

    // all of the function calls above in initVulkan
    void populateDebugMessengerCreateInfo(params);
    void setupDebugMessenger();
    
    VkFormat findSupportedFormat(params);
    VkFormat findDepthFormat();
    bool hasStencilComponent(param);
    void createImage(params);
    void transitionImageLayout(params);
    void copyBufferToImage();
    void createBuffer(params);
    VkCommandBuffer beginSingleTimeCommands();
    void endSingleTimeCommands(param);
    void copyBuffer(params);
    uint32_t findMemoryType(params);
    void updateUniformBuffers(param);
    VkShaderModule createShaderModule(param);
    VkSurfaceFormatKHR chooseSwapSurfaceFormat(param);
    VkPresentModeKHR chooseSwapPresentMode(param);
    VkExtent2D chooseSwapExtent(param);
    SwapChainSupportDetails querySwapChainSupport(param);
    isDeviceSuitable(param);
    bool checkDeviceExtensionSupport(param);
    QueueFamilyIndices findQueuFamilies(param);
    std::vector<const char*> getRequiredExtensions();
    bool checkValidationSupport();
    static std::vector<char> readFiled(param);    
};

These are just the member variables with a few functions declarations and I didn't even show their definitions and the rest of the functions that are used within those functions. Now imagine the line count of all of those functions... some of those functions are 100+ lines long or longer! This made the single App class very bulky, responsible for everything, and was cumbersome navigating through code to find errors...


Vulkan Tutorial v2

As for version 2. I just broke all of this code into about a dozen different classes... one for the devices (physical, logical, surface, window*, debug...), a class for the swap chain and its related objects... a class for the pipeline its layout... a class for the shaders. A another to create the command pool, command buffers, descriptor pools, and descriptor sets... another for all of the buffers such as buffer, index, vertex, buffer memory, etc. A class for the Textures, and one for Model, one for the depth image and image views, etc...

This was easier to navigate through the code, but it also generated a lot of class dependencies and took awareness of object lifetimes, passing around pointers, etc...

Now, this brings me to my current implementation as it looks something like this:


Vulkan Tutorial v3

I now have 2 projects instead of 1.

Project 1 - The main application is quite simple...

// main.cpp

#include "App.h"

int main() {
    try {
        App app;
        app.run("window title", size{x,y});
    }
} 

// App.h

#include "VRXApp.h"

class App : public VRXApp {
private:
    // a couple of members "window title", "program title" for now...
    // eventually it will container more members such as settings for the user
    // members for doing logic, controlling animations or performing actions, etc...
 };

 // App.cpp
 
 // member function implementations... only about 3 to 4 right now...

Project 2 Engine - static lib

 // VRXApp.h

#include "VRXEngine."

class VRXApp {
private:
    // a few members
protected:
    std::unqiue_ptr<VRXEngine> engine;
    /// constructor so as to not be able to declare a VRXApp directly, must be inherited from...
public:
    // a few public methods... some virtual, some purely virtual, some not...     
};

And now we come to the entire point of this demonstration... It is these 2 classes that will illustrate the design here.

// VRX Devices.h
#pragma once

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/vec4.hpp>
#include <glm/mat4x4.hpp>

#include <algorithm>
#include <cstring>
#include <exception>
#include <iostream>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <sstream>
#include <vector>

#ifdef NDEBUG
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif 

namespace vrx {

    constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2;

    const std::vector<const char*> validationLayers = {
        "VK_LAYER_KHRONOS_validation"
    };
    
    const std::vector<const char*> deviceExtensions = {
        VK_KHR_SWAPCHAIN_EXTENSION_NAME
    };

    struct QueueFamilyIndices {
        std::optional<uint32_t> graphicsFamily;
        std::optional<uint32_t> presentFamily;

        bool isComplete() {
            return graphicsFamily.has_value() && presentFamily.has_value();
        }
    };

    struct SwapChainSupportDetails {
        VkSurfaceCapabilitiesKHR capabilities;
        std::vector<VkSurfaceFormatKHR> formats;
        std::vector<VkPresentModeKHR> presentModes;
    };

    class VRXDevices {
    protected:
        VRXDevices() {} // or just make it public and assign it to delete
    public:
        // Instance
        static void createInstance(params);

        // Validation & Support
        static bool checkValidationSupport();
        static std::vector<const char*> getRequiredExtensions();

        // Layers and Debugging
        static void setupDebugMessenger(params);
        static VkResult createDebugUtilsMessengerEXT(params);
        static void populateDebugMessengerCreateInfo(params);
        static void destroyDebugUtilsMessengerEXT(params);
        static  VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(params);

        // Devices (Phyiscal, Logical, Surface, Queue Families & Device Support)
        static QueueFamilyIndices findQueueFamilies(params);
        static void pickPhysicalDevice(params);
        static bool isDeviceSuitable(params);
        static bool checkDeviceExtensionSupport(params);
        static void createLogicalDevice(params);
        static void createSurface(params);

        // Swap Chain & Image Views
        static VkSurfaceFormatKHR chooseSwapSurfaceFormat(params);
        static VkPresentModeKHR chooseSwapPresentMode(params);
        static VkExtent2D chooseSwapExtent(params);
        static SwapChainSupportDetails querySwapChainSupport(params);
        static void createSwapChain(params);
        static void createImageViews(params);
        static void createFrameBuffers(params);
        
        // Pipelines
        static void createRenderPass(params);
        static void createPipeline(params);
    
        // Command Pools, Command Buffers, Semaphores and Fences
        static void createCommandPool(params);
        static void createCommandBuffers(params);
        static void createSyncObjects(params);
    };    

} // namespace vrx

I have just gotten to the point of being able to draw the triangle and to resize the screen... The vertices are still fixed in the shader... I still have yet to implement the vertex, index, and uniform buffers, textures, model loading, etc... Yes, there are quite a lot of parameters being passed to these functions, but this current design pattern within this specific context... does make the code look more elegant, readable, and manageable... I'll get to the technical side in just a bit, but here is what my Engine header looks like...

//VRX Engine.h

#pragma once

#include "VRX Devices.h"

namespace vrx {
    class VRXEngine {
    public:
        const std::vector<std::string_view> shaderFilenames{ "vert.spv", "frag.spv" };
    private:
        GLFWwindow* window_{ nullptr };
        glm::ivec2 windowSize_;    
        VkInstance instance_;
        std::vector<VkExtensionProperties> extensionProps_;
        VkDebugUtilsMessengerEXT debugMessenger_;
        VkSurfaceKHR surface_;    
        VkPhysicalDevice physicalDevice_{ VK_NULL_HANDLE };
        VkDevice device_;    
        VkQueue graphicsQueue_;
        VkQueue presentQueue_;    
        VkSwapchainKHR swapChain_;        
        VkFormat swapChainImageFormat_;
        VkExtent2D swapChainExtent_;
        std::vector<VkImage> swapChainImages_;
        std::vector<VkImageView> swapChainImageViews_;
        std::vector<VkFramebuffer> swapChainFramebuffers_;    
        VkRenderPass renderPass_;
        VkPipelineLayout pipelineLayout_;
        VkPipeline graphicsPipeline_;    
        VkCommandPool commandPool_;
        std::vector<VkCommandBuffer> commandBuffers_;    
        std::vector<VkSemaphore> imageAvailableSemaphores_;
        std::vector<VkSemaphore> renderFinishedSemaphores_;
        std::vector<VkFence> inFlightFences_;
        std::vector<VkFence> imagesInFlight_;
        size_t currentFrame_{ 0 };
        bool framebufferResized_ = false;

    public:
        static void framebufferResizeCallback(GLFWwindow* window, int with, int height) {
            auto app = reinterpret_cast<VRXEngine*>(glfwGetWindowUserPointer(window));
            app->framebufferResized_ = true;
        }

        void createWindow(GLFWwindow* window, glm::ivec2 size) { 
            window_ = window; windowSize_ = size; 
            glfwSetWindowUserPointer(window, this);
            glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
        }
        void initVulkan(std::string_view app_name, std::string_view engine_name, glm::ivec3 app_version = glm::ivec3(1, 0, 0), glm::ivec3 engine_version = glm::ivec3(1, 0, 0));
        void cleanup();
        void reportExtensions();    
        void renderFrame();
        void recreateSwapchain();
        void cleanupSwapchain();      
        // accessors functions for each member... both object and pointer versions.    
    };

} // namespace vrx

Here I can show almost the entire engine.cpp file as it is not overburdened with all of the boilerplate initialization and creation code, etc... It has all of the functionality of the details that it is currently truly responsible for.

// VRX Engine.cpp
#include "VRX Engine.h"
#include "VRX Devices.h"

namespace vrx {

    void VRXEngine::initVulkan(params) {
        VRXDevices::createInstance(params);
        VRXDevices::setupDebugMessenger(params);
        VRXDevices::createSurface(params);
        VRXDevices::pickPhysicalDevice(params);
        VRXDevices::createLogicalDevice(params);
        VRXDevices::createSwapChain(params);
        VRXDevices::createImageViews(params);
        VRXDevices::createRenderPass(params);
        VRXDevices::createPipeline(params);
        VRXDevices::createFrameBuffers(params);
        VRXDevices::createCommandPool(params);
        VRXDevices::createCommandBuffers(params);
        VRXDevices::createSyncObjects(params);
    }

    void VRXEngine::cleanup() {
        cleanupSwapchain();

        for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
            vkDestroySemaphore(device_, renderFinishedSemaphores_[i], nullptr);
            vkDestroySemaphore(device_, imageAvailableSemaphores_[i], nullptr);
            vkDestroyFence(device_, inFlightFences_[i], nullptr);
        }

        vkDestroyCommandPool(device_, commandPool_, nullptr);

        vkDestroyDevice(device_, nullptr);

        if (enableValidationLayers) {
            VRXDevices::destroyDebugUtilsMessengerEXT(instance_, debugMessenger_, nullptr);
        }

        vkDestroySurfaceKHR(instance_, surface_, nullptr);
        vkDestroyInstance(instance_, nullptr);
    }

    void VRXEngine::cleanupSwapchain() {
        for (auto framebuffer : swapChainFramebuffers_) {
            vkDestroyFramebuffer(device_, framebuffer, nullptr);
        }

        vkFreeCommandBuffers(device_, commandPool_, static_cast<uint32_t>(commandBuffers_.size()), commandBuffers_.data());

        vkDestroyPipeline(device_, graphicsPipeline_, nullptr);
        vkDestroyPipelineLayout(device_, pipelineLayout_, nullptr);
        vkDestroyRenderPass(device_, renderPass_, nullptr);

        for (auto imageView : swapChainImageViews_) {
            vkDestroyImageView(device_, imageView, nullptr);
        }

        vkDestroySwapchainKHR(device_, swapChain_, nullptr);
    }

    void VRXEngine::recreateSwapchain() {
        int width = 0;
        int height = 0;
        glfwGetFramebufferSize(window_, &width, &height);
        while (width == 0 || height == 0) {
            glfwGetFramebufferSize(window_, &width, &height);
            glfwWaitEvents();
        }

        vkDeviceWaitIdle(device_);

        cleanupSwapchain();

        VRXDevices::createSwapChain(params);
        VRXDevices::createImageViews(params);
        VRXDevices::createRenderPass(params);
        VRXDevices::createPipeline(params);
        VRXDevices::createFrameBuffers(params);
        VRXDevices::createCommandBuffers(params);
    }    

    void VRXEngine::renderFrame() {
        vkWaitForFences(device_, 1, &inFlightFences_[currentFrame_], VK_TRUE, UINT64_MAX);

        uint32_t imageIndex;
        VkResult result = vkAcquireNextImageKHR(device_, swapChain_, UINT64_MAX, imageAvailableSemaphores_[currentFrame_], VK_NULL_HANDLE, &imageIndex);

        if (result == VK_ERROR_OUT_OF_DATE_KHR) {
            recreateSwapchain();
            return;
        } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
            throw std::runtime_error("failed to acquire swap chain image!");
        }

        if (imagesInFlight_[imageIndex] != VK_NULL_HANDLE) {
            vkWaitForFences(device_, 1, &imagesInFlight_[imageIndex], VK_TRUE, UINT64_MAX);
        }
        imagesInFlight_[imageIndex] = inFlightFences_[currentFrame_];

        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

        VkSemaphore waitSemaphores[] = { imageAvailableSemaphores_[currentFrame_] };
        VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
        submitInfo.waitSemaphoreCount = 1;
        submitInfo.pWaitSemaphores = waitSemaphores;
        submitInfo.pWaitDstStageMask = waitStages;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &commandBuffers_[imageIndex];

        VkSemaphore signalSemaphores[] = { renderFinishedSemaphores_[currentFrame_] };
        submitInfo.signalSemaphoreCount = 1;
        submitInfo.pSignalSemaphores = signalSemaphores;

        vkResetFences(device_, 1, &inFlightFences_[currentFrame_]);

        if (vkQueueSubmit(graphicsQueue_, 1, &submitInfo, inFlightFences_[currentFrame_]) != VK_SUCCESS) {
            throw std::runtime_error("failed to submit draw command buffer!");
        }

        VkPresentInfoKHR presentInfo{};
        presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
        presentInfo.waitSemaphoreCount = 1;
        presentInfo.pWaitSemaphores = signalSemaphores;

        VkSwapchainKHR swapChains[] = { swapChain_ };
        presentInfo.swapchainCount = 1;
        presentInfo.pSwapchains = swapChains;
        presentInfo.pImageIndices = &imageIndex;
      
        result = vkQueuePresentKHR( presentQueue_, &presentInfo);

        if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized_) {
            framebufferResized_ = false;
            recreateSwapchain();
        } else if (result != VK_SUCCESS) {
            throw std::runtime_error("failed to present swap chain image!");
        }

        currentFrame_ = (currentFrame_ + 1) % MAX_FRAMES_IN_FLIGHT;
    }    
} // namespace vrx

Now, as for the implementation details, VRXDevices can not be declared as an object, it's constructor and destructor are deleted. You can not do this:

VRXDevices vrxDevices; // This will fail to compile! There is NO SUCH OBJECT

Now, all of the static functions is this class have static binding and static linkage and there will only ever be 1 declared and defined function regardless of what translation unit they are found in, also this type of class has no member variables. Now, these types of classes could also contain constexpr type name value that are related to the class of functions!

Here the user is forced to use scope resolution operator to use them such as

VRXDevices::createSwapchain(args...);

If these were just standalone functions wrapped in a namespace such as

namespace vrx {
   namespace VRXDevices {
       void createSwapchain(args...);
   }
}

Then a user can easily do this:

using namespace vrx::VRXDevices;

then

createSwapchain();

however, maybe not with this function, but with some other functions there could end up being naming collisions and or resolution problems especially if they start to use aliases... the idea here is the restrict the user for using the using directive on a namespace. Now if I had a bunch of free-standing math type functions such as sqrt, min, max, ceil, floor, etc... then yeah I'd just put them into a namespace... You put values in, it calculates and gives a result back... but not in this case as this is kind of "domain" specific so-to-speak.

Another thing about the concept of wrapping these functions into a nondeclarative class that has complete static binding is that down the road when I start to work on doing multiple shaders and I need multiple pipelines... I'll eventually refactor this like I did in my second version by grouping similar functions such as keeping all of the swap chain stuff together, all of the pipeline stuff together all of the buffers together, the command pools and command buffers, the descriptor pools & descriptor sets, etc... then when I need to start using templates, especially variadic templates... I might have something like:

template<typename... Args>
class VRXShaders {
public:
     enum Type { BLOOM, BLUR, REFLECT, ETC...};
     VRXShaders() = delete;
     ~VRXShaders() = delete;
      static VkShaderModule createShader( Type type, args... );
};

You can not template a namespace... and although you could have a variadic function template, you wouldn't be able to make it have static linkage unless if you put it into a class. Now, when you delete the constructor and the destructor, the class is no longer considered an object, and the user will have to be forced to use the scope resolution operator. This binds related functions to a specific scope resolution.

I do not believe that it is an anti-pattern at all. Now, I'm not trying to claim that this is the perfect solution for every scenario because that is not always the case. Sometimes you might need RAII, you might need CRTP, you might need SFINAE, you might need Polymorphism it all depends on the task at hand. There are places that this kind of structure is very useful, it doesn't affect debugging at all, it keeps the code clean and concise, it helps to make it readable and also expresses intent.

Now can people abuse this technique? Yes, they can and when that happens it can become an issue and possibly a problem. The only major problem here would be when someone goes to refactor it, they would have to move functions, change the resolution to its new scope, change the parameter passing, reorganize things due to change in object lifetimes, etc. So when this is abused, it can become a nuisance and create headaches for some.

I wouldn't say "never" do this, but I also wouldn't say "do this on everything and everywhere". My suggestion on this exact topic is to use this when and where it is appropriate for the problem domain...

You see there are Class Objects such as:

class Foo {
public:
    int x();
    explicit Foo(int val) : x{val} {}
    int me() const { return x; }
}

Foo foo(420);
auto what = foo.me();

And there are Classes of Functions

struct vec2 {
    float x;
    float y;
    
    vec2() : x{0}, y{0} {}
    vec2(float a, float b) : x{a}, y{b} {}

    auto operator[](uint16_t idx) {
        if (idx >= 1) return y;
        else return x;   
    }
}

class transformations {
    transformations();
public:
    static vec2 translate(vec2, vec2);
    static vec2 translate(vec2, float);
    static vec2 translate(float, vec2);
    static vec2 rotate(vec2, float);
    static vec2 rotate(float, vec2);
    static vec2 scale(vec2, float);
    static vec2 scale(float, vec2);
    static float dot(vec2, vec2);
    static vec3  cross(vec2, vec2);
};

There are particular use cases for this technique when certain types of patterns show themselves. It is not so much about should you or shouldn't you use this pattern, it is more about knowing where, when, and how to use them within the appropriate context while possessing a good balance of using other design patterns that fit each problem accordingly!

I demonstrated that OPP can still be used and not violated with the aide of this technique via the VRXEngine class along with the static binding functions that initialize and create the internal components of the VRXEngine class.

When a specific function needs to modify its internal members after creation, that's when you'd need or prefer to have a member function. You can see that within the context of the cleanupSwapChain(), recreateSwapChain() and renderFrame() as these modify the internal members after creation. All of the static functions within the scope of the non-instantiable VRXDevice class only create, populate, or initializes the VRXEngine::member's values before use.

| improve this answer | |

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