Search code examples
c++templatestemplate-instantiation

C++ - Return a template with unknown template argument


I would like to load images from a filetype which can hold many different kinds of data : the pixels can be encoded as floats, doubles, unsigned chars, chars (akin to TIF files). All the processing I'll be doing on the image data after loading depends on the type of the data loaded. I already have a function which can load the appropriate data given a file path and a data type from an external library in C which handles my file type :

/* loads image with pixels of type T in a 1D array (fine for my use case) */
template <typename T> std::vector<T> loadImg(std::string path) {
    std::vector<T> imgData;
    file imgFile = libOpen(path);
    imgData.resize(imgFile.height * imgFile.width);
    libReadImg(imgFile, imgData.data());
    return imgData;
}

What I'd like is to have a 'generic' loader from which the user of my program can specify any kind of supported files they want, and it would load them and define the right type of template to return, at runtime :

/* load the user's selected files : */
template <> /*what do I put here in template argument ?*/
std::vector</*and here ?*/> loadAnyImg(std::string path) {
    file imgFile = openFile(...);
    if (imgFile.type == int) {
        return loadImg<int>(path);
    }
    if (imgFile.type == unsigned char) {
        return loadImg<unsigned char>(path);
    }
    /* ... */
}

Is there a way to do such a thing in C++ ? I've handled templates quite a bit before then but not in this specific use case.

Quick edit : Of course, I could also convert all the data loaded into an internal format with min/max bounds (i.e. convert all pixel data to double with min/max bounds for types other than it). But I'd like to keep the data as untouched as possible


Solution

  • Return something like this:

    std::variant< std::vector<int>, std::vector<unsigned char>, std::vector<double> >
    

    This isn't a template, because which template you call is determined at compile time.

    The caller can get which vector by doing a std::visit and get a compile-time instance of the data.

    The types supported have to be enumerated at compile time, because new code is generated for each type to handle it.


    Variant is a "sum type". It can be any one of the types it contains.

    (If there is an exception it can be valueless in rare cases; a variant of vectors of primitive types will only ever be in that state if you ran out of address space on your computer while doing a few specific operations on the variant; if you only make vectors outside of the variant and move into it, and never copy/assign the variant, that should be a state completely impossible to reach).

    The caller can get the specific vector data out like this:

    bool bWorked = std::visit([&](auto& vec){
      // here `vec` is a vector of concrete type
      return true;
    }, loaded_data);
    

    Now, if you literally want any type (and you don't, trust me), then you could just return a std::any. But callers have to know the exact type in the any in order to interact with it (beyond a copy/move). std::any is usually a bad idea.