Reputation: 2623
I want all my saving and loading of data to go through the same functions to reduce the chance of bugs. To do this I used a lot of templates (and much function overloading). It worked, my code is now much cleaner, but I was unable to use const
for saving (because it goes through the same functions as the loader does, where the data is kept non-const).
I'd like to use the const
correctly, so here is an attempt to get a simple version working, where the data (in this case std::vector
) is non-const for std::ifstream
, and const
otherwise:
#include <iostream>
#include <fstream>
#include <vector>
template <class Foo>
void Overload(const Foo & foo)
{
std::cout << "went to const" << std::endl;
}
template <class Foo>
void Overload(Foo & foo)
{
std::cout << "went to non-const" << std::endl;
}
template <class StreamType, typename... Arguments>
void ReadOrWrite (
/* for 1st argument */ StreamType & filestream,
/* type for 2nd argument */ typename std::conditional<
/* if */ std::is_same<StreamType, std::ifstream>::value,
/* then */ std::vector<Arguments...>,
/* else */ const std::vector <Arguments...>
>::type
/*2nd argument name */ & vector
)
{
Overload(vector);
}
int main ()
{
std::ofstream output_filestream;
std::ifstream intput_filestream;
std::vector<int> vector;
ReadOrWrite(output_filestream, vector);
ReadOrWrite(intput_filestream, vector);
return 0;
}
I know that it will compile/run properly if I edit the function calls to this:
ReadOrWrite<std::ofstream, int>(output_filestream, vector);
ReadOrWrite<std::ifstream, int>(intput_filestream, vector);
But I don't want the user of the function to need to list the types during the function call.
Is there a clean way to do what I'm suggesting?
EDIT
There appears to be a question over the legitimacy of my motive.
I did not explain my motive thoroughly because it's not overly simple (neither is it overly complicated) and I respect the readers' time.
The example I gave was the bare component of what I haven't been able to solve - and the "overload" functions were just included to see if it worked.
However it appears that my lack of explanation has caused confusion, so I will expound:
I have made a small library to handle the general-case saving and loading of data. It successfully allows the users' classes to have simple save/load methods by using the following interface:
class SomeClass
{
public:
template <class StreamType>
void SaveOrLoad(StreamType & filestream)
{
saveload::SaveToOrLoadFromFile(filestream,
data_1_,
data_2_,
/* ..., */
data_n_,
);
}
void SaveToFile (const std::string & filename)
{
std::ofstream output_filestream(filename, std::ios::binary);
// file handling
SaveOrLoad(output_filestream);
}
void LoadFromFile (const std::string & filename)
{
std::ifstream input_filestream(ptf::problem_input_file, std::ios::binary);
// file handling
SaveOrLoad(input_filestream);
}
};
This library handles all fundamental data types, STL containers, and any other containers which use the correct SaveOrLoad(StreamType &)
interface, including saving and resizing of all containers. The library has forced all saves and loads to go through the same deterministic functions, and hence has completely removed the potential for bugs involving of a save/load mismatch (unless the user misuses the library's simple interface).
The problem that I have with my library - and hence the reason for my question - is a theoretical one because I have no need for it presently: The SaveToFile
method should be able to be const
.
Upvotes: 0
Views: 468
Reputation: 2623
Firstly, apologies, my explanation of my problem was bad and I'll do better in future.
Just in case anyone happens to read this and have a similar problem, I solved it by introducing two wrapper functions whose purpose is to explicitly state the template parameters:
template <class DataType>
void ParseData (std::ofstream & output_filestream, const DataType & data)
{
ReadOrWrite<std::ofstream, DataType> (output_filestream, data);
}
template <class DataType>
void ParseData (std::ifstream & input_filestream, DataType & data)
{
ReadOrWrite<std::ifstream, DataType> (input_filestream, data);
}
The important part is that this solution is scaleable: To handle each new data type I only need to write one ReadOrWrite
function, with no unnecessary template parameters at function calls.
How the ParseData
functions fit into the solution:
#include <iostream>
#include <fstream>
#include <vector>
// lots of useful typetraits:
#include <type_traits>
template <class S, class T = void>
struct is_vector : std::false_type {};
template <class S, class T>
struct is_vector <std::vector<S,T>> : std::true_type {};
template <typename Datatype>
inline constexpr bool is_vector_v = is_vector<Datatype>::value;
// Kinda similar format to my serialization library:
template <class Foo>
void Overload(const Foo & foo)
{
std::cout << "went to const" << std::endl;
}
template <class Foo>
void Overload(Foo & foo)
{
std::cout << "went to non-const" << std::endl;
}
// Special type trait specific to this library
// (within an anonymous namespace)
template <typename, typename Data>
struct vet_data_constness
: std::add_const<Data> {};
template <typename Data>
struct vet_data_constness <std::ifstream, Data>
: std::remove_const<Data> {};
template <typename StreamType, typename Data>
using vet_data_constness_t
= typename vet_data_constness<StreamType,Data>::type;
template <class StreamType, typename DataType>
std::enable_if_t<is_vector_v<DataType>>
ReadOrWrite (StreamType & filestream,
vet_data_constness_t<StreamType, DataType> & vector)
{
Overload(vector);
}
// These functions are simply for routing back to the ReadOrWrite
// funtions with explicit calls
template <class DataType>
void ParseData (std::ofstream & output_filestream, const DataType & data)
{
ReadOrWrite<std::ofstream, DataType> (output_filestream, data);
}
template <class DataType>
void ParseData (std::ifstream & input_filestream, DataType & data)
{
ReadOrWrite<std::ifstream, DataType> (input_filestream, data);
}
int main ()
{
std::ofstream output_filestream;
std::ifstream intput_filestream;
std::vector<int> vector;
ParseData(output_filestream, vector);
ParseData(intput_filestream, vector);
return 0;
}
Upvotes: 0
Reputation: 41760
The best suggestion would be to provide two separated functions, since reading and writing are two distinct operations, no matter what type you sent in it. For example, someone could be fstream
for both input and output. Simply by the type system, you can't know the intent. The decision of reading or writing is usually an intent, and these are rarely embeddable into the type system.
Since saving and loading are distinct operation, it should be distinct functions (possibly sharing code between them)
If you really want a function that do both and switch between the types, then I'd suggest constrain the functions for input or output:
// output streams
template <class StreamType, typename... Arguments,
typename std::enable_if<std::is_base_of<std::ostream, StreamType>::value, int>::type = 0
>
void ReadOrWrite (
StreamType & filestream,
std::vector<Arguments...> const& vector
) {
Overload(vector);
}
// input streams
template <class StreamType, typename... Arguments,
typename std::enable_if<std::is_base_of<std::istream, StreamType>::value, int>::type = 0
>
void ReadOrWrite (
StreamType& inputstream,
std::vector<Arguments...>& vector
) {
Overload(vector);
}
Since the second is more specialized than the first, it will be taken whenever the stream is std::istream
and the vector is mutable. Otherwise, the first one is taken.
Upvotes: 3
Reputation: 66200
Another overload solution could transform ReadOrWrite()
, almost as you have written in your question, in an helper function
template <typename ... Args, typename ST>
void ReadOrWrite_helper (ST &, typename std::conditional<
std::is_same<ST, std::ifstream>::value,
std::vector<Args...>,
std::vector<Args...> const>::type vec)
{ Overload(vec); }
adding a overloaded couple of ReadOrWrite()
function to select select the Args...
and explicit them calling the helper function
template <typename ... Ts>
void ReadOrWrite (std::ifstream & is, std::vector<Ts...> & vec)
{ ReadOrWrite_helper<Ts...>(is, vec); }
template <typename ... Ts>
void ReadOrWrite (std::ofstream & is, std::vector<Ts...> const & vec)
{ ReadOrWrite_helper<Ts...>(is, vec); }
Observe that, given that the Args...
types are in non deduced context so are to explicated, I've placed they, in the ReadOnWrite_helper()
template parameter declaration, before ST
; so there is no need to explicit also ST
.
Observe also that if you don't need to know the Args...
types inside ReadOrWrite_helper()
, all became simpler
template <typename V, typename ST>
void ReadOrWrite_helper (ST &, V & vec)
{ Overload(vec); }
template <typename V>
void ReadOrWrite (std::ifstream & is, V & vec)
{ ReadOrWrite_helper(is, vec); }
template <typename V>
void ReadOrWrite (std::ofstream & is, V const & vec)
{ ReadOrWrite_helper(is, vec); }
and also disappear the needs of explicating the V
type.
Upvotes: 2