There really shouldn't be that much to convert as there are containers, functions and algorithms in the stl that already exist that will do this for you. With out any function examine this short program:
#include <vector>
#include <numeric>
#include <iostream>
#include <exception>
int main() {
try {
std::vector<uint64_t> values{ 1,2,3,4,5,6,7,8,9,10,11,12 };
int total = std::accumulate( values.begin(), values.end(), 0 );
uint64_t average = static_cast<uint64_t>( total ) / values.size();
std::cout << average << '\n';
} catch( const std::runtime_error& e ) {
std::cerr << e.what() << '\n';
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
On my machine windows 7 ultimate 64bit
running visual studio 2017 CE
compiled with language version set to most recent c++17
or greater. This does give me a compiler warning! Warning: C4244
generated due to conversion and possible loss of data. However there are no compiler errors and it does run and give the expected result. The output here is 6
as expected since integer division
is truncated. If I change these lines of code above to this:
double total = std::accumulate( values.begin(), values.end(),
static_cast<double>( 0 ) );
double average = total / values.size();
It fixes the compiler warnings above by adding the static_cast
and it sure enough prints out 6.5
which is the actual value.
This is all fine and good since the vector is already initialized with values; however, this may not be always the case so let's move this into a function that will take an arbitrary array. It would look something like this:
uint64_t array_average( std::vector<uint64_t>& values ) {
// Prevent Division by 0 and early return
// as to not call `std::accumulate`
if ( !values.empty() ) {
// check if only 1 entry if so just return it
if ( values.size() == 1 ) {
return values[0];
} else { // otherwise do the calculation.
return std::accumulate( values.begin(), values.end(),
static_cast<uint64_t>( 0 ) ) / values.size();
}
}
// Empty Container
throw std::runtime_error( "Can not take average of an empty container" );
}
This function is nice and all, we can do better by improving this by making it a little more generic that will work with any arithmetic type
!
template<typename T>
T array_average( std::vector<T>& values ) {
if( std::is_arithmetic<T>::value ) {
if( !values.empty() ) {
if( values.size() == 1 ) {
return values[0];
} else {
return std::accumulate( values.begin(), values.end(), static_cast<T>( 0 ) ) / values.size();
}
} else {
throw std::runtime_error( "Can not take average of an empty container" );
}
} else {
throw std::runtime_error( "T is not of an arithmetic type" );
}
}
At first glance this looks okay. This will compile and run if you use this with types that are arithmetic
. However, if we use it with a type that isn't this will fail to compile. For example:
#include <vector>
#include <numeric>
#include <iostream>
#include <exception>
#include <type_traits>
class Fruit {
protected:
std::string name_;
public:
std::string operator()() const {
return name_;
}
std::string name() const { return name_; }
Fruit operator+( const Fruit& other ) {
this->name_ += " " + other.name();
return *this;
}
};
class Apple : public Fruit {
public:
Apple() { this->name_ = "Apple"; }
};
class Banana : public Fruit {
public:
Banana() { this->name_ = "Banana"; }
};
class Pear : public Fruit {
public:
Pear() { this->name_ = "Pear"; }
};
std::ostream& operator<<( std::ostream& os, const Fruit& fruit ) {
os << fruit.name() << " ";
return os;
}
template<typename T>
T array_average( std::vector<T>& values ); // Using the definition above
int main() {
try {
std::vector<uint64_t> values { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
std::vector<double> values2 { 2.0, 3.5, 4.5, 6.7, 8.9 };
std::vector<Fruit> fruits { Apple(), Banana(), Pear() };
std::cout << array_average( values ) << '\n'; // compiles runs and prints 6
std::cout << array_average( values2 ) << '\n'; // compiles runs and prints 5.12
std::cout << array_average( fruits ) << '\n'; // fails to compile.
} catch( const std::runtime_error& e ) {
std::cerr << e.what() << '\n';
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
This fails to compile because the static_cast
can not convert int
to T
with T = Fruit
MSVC
compiler error C2440
We can fix this by changing a single line of code in our function template if your compiler supports it:
We can change if( std::is_arithmetic<T>::value )
to if constexpr( std::is_arithmetic<T>::value )
and our function will now look like this:
template<typename T>
T array_average( const std::vector<T>& values ) {
if constexpr( std::is_arithmetic<T>::value ) {
if( !values.empty() ) {
if( values.size() == 1 ) {
return values[0];
} else {
return std::accumulate( values.begin(), values.end(), static_cast<T>( 0 ) ) / values.size();
}
} else {
throw std::runtime_error( "Can not take average of an empty container" );
}
} else {
throw std::runtime_error( "T is not of an arithmetic type" );
}
}
You can run the same program above and it will fully compile even when you are using types that are not arithmetic.
int main() {
//....
std::cout << array_average( fruits ) << '\n'; // Now compiles
//...
}
However when you run this code it will generate a Runtime Error and depending on how your IDE and debugger is setup you may need to put a break point within the catch
statement where the return EXIT_FAILURE
is to see the message printed to the screen, otherwise the application may just exit without any notification at all.
If you don't want runtime errors you can substitute and produce compiler time errors by using static_assert instead of throwing a runtime error. This can be a handy little function, but it isn't 100% without some minor limitations and gotchas, but to find out more information about this function you can check the Question that I had asked when I was writing the implementation to this function that can be found here and you can read the comments there that will give you more insight to some of the limitations that this function provides.
One of the current limitations with this function would be this: let's say we have a container that has a bunch of complex numbers (3i + 2)
, (4i - 6)
, (7i + 3)
well you can still take the average of these as it is a valid thing, but the above function will not consider this to be arithmetic in it's current state.
To resolve this issue what can be done is this: instead of using std::is_arithmetic<t>
you could write your own policy
and traits
that this function should accept. I'll leave that part as an exercise for you.
As you can see a majority of the work is already being done for us with the standard library. We used accumulate
and divided by the containers size and we were done, the rest of the time involved was making sure it accepts proper types, if it's to be thread safe and or exception safe etc.
Finally we did not have to worry about cumbersome for loops on arrays and making sure the loops didn't exceed the size of the array. We did not have to call new
and worry about when and where to call delete
in order to not have any memory leaks. ASFAIK I do not think that std::accumulate
will overflow on supporting containers, but don't quote me on this one. It may depend on the types
that are in the container and that there is a static_cast
involved. Even with some of these caveats in many cases it is still better to use containers than managing your own raw memory, and to use the algorithms and functions that are designed to work on them. They make things a lot simpler and easier to manage and even debug.