C++ equivalence of Python code for HDF5. Working with datasets resizing, working with indexes [i][j] in array-like structures, etc

This post is about code conversion from Python to C++ about HDF5, trying to keep the C++ part simplified, and this about a specific part from a previous post here, just in order to separate that whole content in separate and more specific sub-questions.

General process in the program about HDF5:

  • Generate a .h5 file with one or more datasets.
  • Writing/reading if the .h5 file exists, and if not, then creating the .h5 file and writing/reading.
  • Each dataset in the file will contain data that comes from a 2d-collection with variable size.
  • The 2d-collection to insert in the dataset is returned by a function and we don’t know its exact size in advance (at compilation time). In some parts of the program we could know only the number of rows in advance and in other parts we could know only the number of columns in advance. This is the reason because the C++ code below shows tests with different ā€œ2-dimensionalā€ data structures, because in C++ you have more options for different cases compared to Python.

Main question for this post:

What is the C++ equivalence for the following Python section (full code below):

f[dset_name].resize(f[dset_name].shape[0] + a_2d.shape[0], axis=0)
f[dset_name][-a_2d.shape[0]:] = a_2d

Python code to convert to C++:

import numpy as np
import h5py


file_name = "f_1"
dset_name = "dset_1"

# list-of-lists representing a function call that returns a list-of-lists with variable size.
l_2d = [[1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4]]

f = h5py.File(f"{file_name}.h5", "a")
# f.flush()

# Create dataset
if dset_name not in f.keys():
  f.create_dataset(dset_name, (0, 3), maxshape=(None, 3), dtype="i")
  # f.flush()

# Write data
a_2d = np.array(l_2d)

f[dset_name].resize(f[dset_name].shape[0] + a_2d.shape[0], axis=0)
f[dset_name][-a_2d.shape[0]:] = a_2d

f.flush()
f.close()

C++ code, progress for now:

#include <array>
#include <vector>
#include "H5Cpp.h"

const H5std_string FILE_NAME("f_1.h5");
const H5std_string DATASET_NAME("dset_1");

int main()
{
  // Tests with different data structures
  // int                               a_2d[4][3] = {{1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4}};
  // std::array<std::array<int, 3>, 4> a_2d{{ {1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4} }};
  std::vector<std::array<int, 3>>      a_2d{{1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4}};
  // std::array<std::vector<int>, 4>   a_2d{{ {1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4} }};
  // std::vector<std::vector<int>>     a_2d{{1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4}};
  
  try
  {
    Exception::dontPrint();
    
    H5::H5File f(FILE_NAME, H5F_ACC_TRUNC);  // AFAIK `H5F_ACC_TRUNC` wouldn't be the equivalence for the `"a"` mode in `h5py.File(f"{file_name}", "a")`
    
    
    hsize_t dims[2];
    dims[0] = 4;  // a_2d.size();
    dims[1] = 3;  // a_2d[0].size();
    H5::DataSpace dspace(2, dims);
    
    H5::DataSet dset = f.createDataSet(DATASET_NAME, H5::PredType::NATIVE_INT32, dspace);  // `H5::PredType::STD_I32BE`
    
    // dset.write(a_2d, H5::PredType::NATIVE_INT32);     // for C-style arrays
    dset.write(a_2d.data(), H5::PredType::NATIVE_INT32); // for non C-style arrays
    
    dspace.close();  // After checking some HDF5 C++ API examples in github,
    dset.close();    // it is not clear exactly what is needed about the
                     // `close` and `flush`
    
    // f.flush();  // Not tested because it asks for `H5F_scope_t scope`
    f.close();
    
  }

  catch (FileIException error)
  {
    error.printErrorStack();
    return -1;
  }

  catch (DataSetIException error)
  {
    error.printErrorStack();
    return -1;
  }

  catch (DataSpaceIException error)
  {
    error.printErrorStack();
    return -1;
  }

  return 0; // successfully terminated
}


About the HDF5 C++ API

I’m new with HDF5 and when searching for online information about the HDF5 C++ API documentation (this and other parts in https://support.hdfgroup.org), code examples from the documentation, and in general when searching for online information about HDF5 (another example here), I can notice the content and information about the C++ API is not always as complete as the content for the HDF5 C API. Just mentioning a mere illustration of this, when you compare the same code example from the documentation: in C and in Python, with the equivalent example in C++, I notice the C++ code example doesn’t content the parts about to close a dataspace and to close a file, and I mention this mainly for the purpose of highlighting that it would be great if the examples from the documentation had a 1:1 equivalence among the different languages, showing basic aspects that are present in this post, like the equivalence in C and C++ of the Python ā€œaā€ mode (ā€œRead/write if exists, create otherwiseā€), or when to use the flush functionality, etc.

@mlt.2 , welcome to a forum where we try to help each other. We try to offer the best advice we can, and that sometimes means politely and constructively pointing out that we believe someone is on the wrong track.

I cannot, in good conscience, recommend using a C++ (98) legacy interface for HDF5. (You’d be better off sticking w/ C).

There are several fine HDF5 interfaces for modern C++, including H5CPP, h5cpp, and HighFive. Go, check them out!

Your Python example looks a bit like C code, and could be formatted like this:

import h5py
import numpy as np


FILE_NAME = "f_1.h5"
DATASET_NAME = "dset_1"

# list-of-lists representing a function call that returns a list-of-lists with variable size.
ROWS = [[1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4]]


def main() -> None:
    data = np.asarray(ROWS, dtype=np.int32)

    with h5py.File(FILE_NAME, "a") as h5file:
        if DATASET_NAME in h5file:
            dataset = h5file[DATASET_NAME]
        else:
            dataset = h5file.create_dataset(
                DATASET_NAME,
                shape=(0, data.shape[1]),
                maxshape=(None, data.shape[1]),
                dtype=data.dtype,
            )

        start = dataset.shape[0]
        stop = start + data.shape[0]

        dataset.resize(stop, axis=0)
        dataset[start:stop] = data

        h5file.flush()


if __name__ == "__main__":
    main()

I would expect my C++ counterpart to look as idiomatic and concise. So how about this H5CPP version:

#include <h5cpp/all>

#include <fstream>
#include <stdexcept>

constexpr auto FILE_NAME = "f_1.h5";
constexpr auto DATASET_NAME = "dset_1";

constexpr int ROWS[][3] = {
	{1, 1, 1},
	{2, 2, 2},
	{3, 3, 3},
	{4, 4, 4},
};

int main() {
	constexpr hsize_t row_count = sizeof(ROWS) / sizeof(ROWS[0]);
	constexpr hsize_t col_count = sizeof(ROWS[0]) / sizeof(ROWS[0][0]);

	h5::fd_t file = std::ifstream(FILE_NAME).good()
		? h5::open(FILE_NAME, H5F_ACC_RDWR)
		: h5::create(FILE_NAME, H5F_ACC_TRUNC);

	h5::mute();
	const bool exists = H5Lexists(static_cast<hid_t>(file), DATASET_NAME, H5P_DEFAULT) > 0;
	h5::unmute();

	h5::ds_t dataset = exists
		? h5::open(file, DATASET_NAME)
		: h5::create<int>(
			file,
			DATASET_NAME,
			h5::current_dims{0, col_count},
			h5::max_dims{H5S_UNLIMITED, col_count},
			h5::chunk{1, col_count});

	h5::current_dims_t dims;
	h5::get_simple_extent_dims(h5::get_space(dataset), dims);
	if (dims.size() != 2 || dims[1] != col_count)
		throw std::runtime_error("dset_1 must have shape (N, 3)");

	const hsize_t start = dims[0];
	const hsize_t stop = start + row_count;

	h5::set_extent(dataset, h5::current_dims{stop, col_count});
	h5::write(dataset, &ROWS[0][0], h5::offset{start, 0}, h5::count{row_count, col_count});

	H5Dflush(static_cast<hid_t>(dataset));
	H5Fflush(static_cast<hid_t>(file), H5F_SCOPE_LOCAL);
}

There is even an append function to make this even more concise.

The C++98 version is more boilerplate than anything else, and should give you pause.

Best, G.

3 Likes

@gheber Thanks a lot for your detailed response, I really appreciate to recieve a response from a experienced developer :slight_smile:


Please let me describe a bit more about the general workflow context

  • Reasons to work with C++ and with the HDF5 C++ API:

    • ā€œTheoriticalā€ general simplification and also with no the need to make type convertions to C. For now, I notice that working with HDF5 C++ API seems to be an exception for this.
    • One of the main reasons is not being limited to work with C-style arrays, trying to be able to work with variable-length C++ data structures (when we don’t know the exact lenght in advance) like std::vector<T>, std::valarray<T>, std::vector<std::vector<T>>, std::vector<std::array<T, M>>, std::array<std::vector<T>, N>, and also with others like std::array<T, N>, std::array<std::array<T, M>, N>. So, trying that the work with the data collections be simplified, without new, delete, pointers, with simplified functions calls, being intuitive, etc.
  • I understand the benefits of working with C++ non-built-in libraries like boost or Eigen that for example include multidimensional data structures, however, it is always very welcome to be able to work with what is built-in with C++ and in general with the minimum of dependencies, for those who have some preference to work in that way.



Before to start with the HDF5 C++ API and with the preference to work with an official API, I was thinking about the possibility to work with the HDF5 C API in the C++ programs and when working with C++ data structures like the listed above (this would be the most frequent case), once the collection be ready to be inserted in the HDF5 file, then copying the collection (for example a vector-of-arrays) into a C-style array-of-array T[N][M]. This concept would be ā€œsimilarā€ to the process in the Python program here where a list-of-list is copied into a NumPy-array. The clear disadvantage of this would be precisely the need for a copy, with a for loop that sometimes would work with thousands (or more) of elements in each interaction with HDF5, so making the process more inefficient. However, could you please tell me your point of view about this option also showing the program equivalence when working with the HDF5 C API in a C++ program under this context, in order to have the parts about to extent, the indexes [i][j], check if a dataset is not present in a file, etc.


Thank you for your time!

I wish I were. :grinning_face:

H5CPP comes with very thorough type-system documentation. In the context of interoperability, a solid type system is 80%+ of the job.

(I’m more familiar w/ H5CPP, hence biased. You should compare w/ the other options.)

I’m new with HDF5 and I had been learning about the Python API h5py, but now I’m looking for move that Python knowledge to C++.

Now I feel a bit lost because I had been for a week searching for information about the HDF5 C++ API and making testing with the hope to be able to work with the official C++ API and with C++ variable length containers.

@gheber and the big question that comes to my mind is, why not ā€œmergeā€ or even unify those C++ development efforts (or some of them) like ā€œH5CPPā€, ā€œh5cppā€, ā€œHighFiveā€, and make it/them part of the official HDF5 library? or make one them co-official or something like that, maybe I don’t know what would be the exact situation that would apply but I think the idea is here, because in that way the users could work directly with 1 single official HDF5 library-API also knowing that is a project with more ā€œguarantiesā€ for the long term, 5, 10, 20+ years later. Of course I’m not an expert and of course I can understand there would be reasons to make things in a certain way, so please take this as a very friendly question.

This question is more or less related to other situations I notice in C++ in general, that in contrast with other languages where many aspects come built-in (like JSON, Sockets, etc.), I notice in C++ there are many 3rd-party libraries for the same purpose and then there are forks for those 3rd-party libraries, and also wrappers/adapters for those 3rd-party libraries. I can perfectly understand this creates a rich community ecosystem, sure, but then maybe for this reason you also see projects without maintenance since years ago, etc.

Thank you and have a good weekend!

1 Like

Hi @mlt.2,

If you’re open to shifting programming paradigms—from imperative to declarative—you might want to consider using HDFql. With it, your C++ program (based on @gheber’s example) could be simplified to something like the following:

// include HDFql C++ header file (make sure it can be found by the C++ compiler)
#include "HDFql.hpp"


#define FILE_NAME "f_1.h5"

#define DATASET_NAME "dset_1"

int ROWS[][3] = {{1, 1, 1}, {2, 2, 2},	{3, 3, 3}, {4, 4, 4}};


int main()
{

	// declare variables
	char script[1024];
	int row_count = sizeof(ROWS) / sizeof(ROWS[0]);
	int col_count = sizeof(ROWS[0]) / sizeof(ROWS[0][0]);


	// check if file FILE_NAME exists (if it does, open it; otherwise, if it does not, create and open it)
	sprintf(script, "SHOW FILE %s", FILE_NAME);
	if (HDFql::execute(script) == HDFql::Success)
	{
		sprintf(script, "USE FILE %s", FILE_NAME);
	}
	else
	{
		sprintf(script, "CREATE AND USE FILE %s", FILE_NAME);
	}
	HDFql::execute(script);


	// check if dataset DATASET_NAME exists (if it does not, create it)
	sprintf(script, "SHOW DATASET %s", DATASET_NAME);
	if (HDFql::execute(script) != HDFql::Success)
	{
		sprintf(script, "CREATE DATASET %s AS INT(0 TO UNLIMITED, %d)", DATASET_NAME, col_count);
		HDFql::execute(script);
	}


	// extend first dimension of dataset DATASET_NAME by row_count
	sprintf(script, "ALTER DIMENSION %s TO +%d", DATASET_NAME, row_count);
	HDFql::execute(script);


	// write (append) ROWS into DATASET DATASET_NAME
	sprintf(script, "INSERT INTO DATASET %s[-%d:::] VALUES FROM MEMORY %d", DATASET_NAME, row_count, HDFql::variableTransientRegister(&ROWS));
	HDFql::execute(script);


	return EXIT_SUCCESS;

}

Hope it helps!

1 Like

Hello @contact !

Please note that what follows I say with respect.

When I started to read about HDFql it was an instant very good impression, … until I couldn’t find to be an open source option (if I’m wrong, please correct me). And once I visited the site, the firt question that came to my mind was if the HDFql team has considered to make the project open source with an equivalent license as HDF5, or maybe in a similar way like other database projects, about being open source with the option for a paid support for big organizations or something like that. Just as ideas.


I’ve been searching for information about HDF5 during some time and I’ve read good reviews about it, and for now and taking into consideration I’m new in HDF5, the main part where I see more potential is about the C++ part, maybe making part of the official development one or more of those projects: ā€œH5CPPā€, ā€œh5cppā€, ā€œHighFiveā€, or maybe having HDFql as solid open source option.


One of the repeated reasons I see some programmers choose C++ or C for certain type of projects, apart from the aspect of speed etc. is because in contrast with other languages like Javascript* (putting a clear and random example), with C++ or C the result has good chances to keep perfectly working during years without the need of major updates about the external libraries and knowing that if 5 years later some update is needed for any reason, if you put a minimum of planification in first place, then there are good probabilities about the libraries you chose be still in active development (even you being part of the development team in case you wish or be allowed, why not).

I’ve seen projects working with HDF5 C API with code with 20 years or more, and it would be great to have that kind of perception of ā€œsolidityā€ (to call it in some way) about a C++ API for HDF5 (sorry, my English is not the best but I think you can understand the general idea).


Thanks for showing an interesting alternative!


* Javascript is great for other scenarios.

1 Like

Thank you for the thoughtful post, and no need to apologize for your English, the ideas came through clearly.

Concerning the C++ API, we are currently planning to move away from its active development as our primary API in the future. As a result, it is not being developed as extensively and lacks the features found in our other wrappers. The rationale behind this decision is clear: the strength of HDF5 lies in its C API, which is stable, well-tested, and has been reliably used for over 20 years. Maintaining a first-party C++ layer on top of that adds surface area without adding much that a well-designed header-only wrapper cannot do better.

The stability argument you are presenting actually supports this approach: a lightweight, header-only wrapper over a 20-year-old stable C API benefits from that stability. Conversely, a more complex first-party C++ binding becomes its own maintenance challenge.

Going forward, we are looking at how best to support these community wrappers.

1 Like

I’m not putting words in @brtnfld’s or anyone else’s mouth, but I find the term ā€˜wrapper’ applied to these language bindings in general, and C++ in particular, somewhat demeaning. I find it demeaning because these bindings, when done right, are value amplifiers, enhancers, and creators in their own right. If they just ā€œrepeatā€ the C-API, the only ā€œvalueā€ they deliver is that a programmer in another language can make programs look more like C.

(Modern) C++ in particular is different, in that not only can it provide idiomatic bindings for the HDF5 C-API and other APIs (e.g., linear algebra packages), it can provide compile-time(!) performance portable abstractions, that get lost in the jungle of 100s of property lists, and by asking users questions they are in no position to answer correctly. To use a hardware metaphor, C is the world of gates (and don’t forget to bring your own data structures!), C++ is the world of FPGAs/ICs. And, C++ creates space to play with the C-API and to tease out what is sound, misaligned, misfactored, or doesn’t belong in the C-API.

There is a lot more to say here, but this is not the topic of this thread. It’s also legitimate to ask what the goal of a given foreign (=non-C) API is. There can be different goals, and there’s no point in arguing with what users are trying to achieve. Is interoperability with other languages a goal? Is completeness of C-API coverage a good goal? It’s not good or bad; it depends. If your goal is to help users avoid mistakes, it’s almost a sure way to miss that goal. ( :police_car_light: Bias alert!) H5CPP adopted a rather clever conversion policy that allows controlled fallback to the raw C-API, should that be necessary, without wrapping the whole C-API.

Let’s have a separate thread to talk about goals for a modern C++ library for HDF5 I/O.

G.

2 Likes