Where are the tests?
The code doesn't compile - I needed to add a missing semicolon in Solution::solve()
and prefix with some definitions:
#include <vector>
#include <string>
#include <queue>
#include <unordered_map>
#include <functional>
using std::vector;
using std::string;
using std::unordered_map;
using std::priority_queue;
using std::greater;
namespace Solution
{
std::vector<double> solve(std::vector<int>&);
}
(Though really I wouldn't have all those using
s - just write the full names instead).
Even with these fixes, I get a lot of compiler warnings:
269757.cpp: In function ‘std::vector<double> Solution::solve(std::vector<int>&)’:
269757.cpp:142:19: warning: conversion from ‘std::vector<int>::size_type’ {aka ‘long unsigned int’} to ‘int’ may change value [-Wconversion]
142 | int n = A.size();
| ~~~~~~^~
269757.cpp: In instantiation of ‘class ActionInterface<int>’:
269757.cpp:35:7: required from ‘class Action<int, double>’
269757.cpp:102:7: required from ‘class RunningMedian<int, double>’
269757.cpp:144:66: required from here
269757.cpp:28:7: warning: ‘class ActionInterface<int>’ has virtual functions and accessible non-virtual destructor [-Wnon-virtual-dtor]
28 | class ActionInterface{
| ^~~~~~~~~~~~~~~
269757.cpp: In instantiation of ‘class Action<int, double>’:
269757.cpp:102:7: required from ‘class RunningMedian<int, double>’
269757.cpp:144:66: required from here
269757.cpp:35:7: warning: base class ‘class ActionInterface<int>’ has accessible non-virtual destructor [-Wnon-virtual-dtor]
35 | class Action : public ActionInterface<T> {
| ^~~~~~
269757.cpp:35:7: warning: ‘class Action<int, double>’ has virtual functions and accessible non-virtual destructor [-Wnon-virtual-dtor]
269757.cpp: In instantiation of ‘class RunningMedian<int, double>’:
269757.cpp:144:66: required from here
269757.cpp:102:7: warning: base class ‘class Action<int, double>’ has accessible non-virtual destructor [-Wnon-virtual-dtor]
102 | class RunningMedian : public Action<T,V>
| ^~~~~~~~~~~~~
269757.cpp:102:7: warning: ‘class RunningMedian<int, double>’ has virtual functions and accessible non-virtual destructor [-Wnon-virtual-dtor]
269757.cpp: In instantiation of ‘Stream<T>::Stream() [with T = int]’:
269757.cpp:143:43: required from here
269757.cpp:70:9: warning: ‘Stream<int>::buffer’ should be initialized in the member initialization list [-Weffc++]
70 | Stream(){};
| ^~~~~~
269757.cpp:70:9: warning: ‘Stream<int>::actions’ should be initialized in the member initialization list [-Weffc++]
269757.cpp: In instantiation of ‘RunningMedian<T, V>::RunningMedian() [with T = int; V = double]’:
269757.cpp:144:66: required from here
269757.cpp:109:9: warning: ‘RunningMedian<int, double>::max_heap’ should be initialized in the member initialization list [-Weffc++]
109 | RunningMedian() {};
| ^~~~~~~~~~~~~
269757.cpp:109:9: warning: ‘RunningMedian<int, double>::min_heap’ should be initialized in the member initialization list [-Weffc++]
269757.cpp: In instantiation of ‘Stream<T>::~Stream() [with T = int]’:
269757.cpp:154:12: required from here
269757.cpp:95:17: warning: deleting object of abstract class type ‘ActionInterface<int>’ which has non-virtual destructor will cause undefined behavior [-Wdelete-non-virtual-dtor]
95 | delete action;
| ^~~~~~~~~~~~~
Don't ignore warnings - your compiler is your first reviewer.
For example, that last one causes a memory leak:
==2921039== 32 bytes in 1 blocks are definitely lost in loss record 1 of 2
==2921039== at 0x4839F2F: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==2921039== by 0x10E515: __gnu_cxx::new_allocator<int>::allocate(unsigned long, void const*) (new_allocator.h:127)
==2921039== by 0x10D354: allocate (allocator.h:201)
==2921039== by 0x10D354: std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) (alloc_traits.h:460)
==2921039== by 0x10C929: std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (stl_vector.h:346)
==2921039== by 0x10C5AF: void std::vector<int, std::allocator<int> >::_M_realloc_insert<int const&>(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&) (vector.tcc:440)
==2921039== by 0x10BA83: std::vector<int, std::allocator<int> >::push_back(int const&) (stl_vector.h:1198)
==2921039== by 0x10F157: std::priority_queue<int, std::vector<int, std::allocator<int> >, std::greater<int> >::push(int const&) (stl_queue.h:642)
==2921039== by 0x10EF2B: RunningMedian<int, double>::process(int) (269757.cpp:114)
==2921039== by 0x10BB1E: Stream<int>::processActions() (269757.cpp:59)
==2921039== by 0x10B0B7: Stream<int>::addElement(int) (269757.cpp:75)
==2921039== by 0x10A3F0: Solution::solve(std::vector<int, std::allocator<int> >&) (269757.cpp:140)
==2921039== by 0x10A58F: main (269757.cpp:153)
==2921039==
==2921039== 32 bytes in 1 blocks are definitely lost in loss record 2 of 2
==2921039== at 0x4839F2F: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==2921039== by 0x10E515: __gnu_cxx::new_allocator<int>::allocate(unsigned long, void const*) (new_allocator.h:127)
==2921039== by 0x10D354: allocate (allocator.h:201)
==2921039== by 0x10D354: std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) (alloc_traits.h:460)
==2921039== by 0x10C929: std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (stl_vector.h:346)
==2921039== by 0x10C5AF: void std::vector<int, std::allocator<int> >::_M_realloc_insert<int const&>(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&) (vector.tcc:440)
==2921039== by 0x10BA83: std::vector<int, std::allocator<int> >::push_back(int const&) (stl_vector.h:1198)
==2921039== by 0x10F105: std::priority_queue<int, std::vector<int, std::allocator<int> >, std::less<int> >::push(int const&) (stl_queue.h:642)
==2921039== by 0x10EFE3: RunningMedian<int, double>::process(int) (269757.cpp:121)
==2921039== by 0x10BB1E: Stream<int>::processActions() (269757.cpp:59)
==2921039== by 0x10B0B7: Stream<int>::addElement(int) (269757.cpp:75)
==2921039== by 0x10A3F0: Solution::solve(std::vector<int, std::allocator<int> >&) (269757.cpp:140)
==2921039== by 0x10A58F: main (269757.cpp:153)
We don't need a modifiable copy of the input vector, so accept it by reference to const. Consider accepting arbitrary ranges:
std::vector<double> solve(std::ranges::forward_range auto);
Transferring ownership using raw pointers is error-prone and therefore dangerous. Use smart pointers instead to indicate the ownership semantics (e.g. pass a unique pointer by value when transferring ownership from one object to another).
Better still, avoid pointers altogether.
Consider
using Action<T,V>::result;
That lets us write result
instead of the ugly this->result
all over the place.
Don't write empty default constructors. Just let the compiler default them naturally.
When overriding functions, use the override
keyword instead of virtual
.
Don't cast std::size_t
to int
like this:
if(abs((int)max_heap.size() - (int)min_heap.size()) > 1){
if(max_heap.size() > min_heap.size()){
That's probably better split out into simpler tests anyway:
if (max_heap.size() > min_heap.size() + 1) {
min_heap.push(max_heap.top());
max_heap.pop();
}
if (min_heap.size() > max_heap.size() + 1) {
max_heap.push(min_heap.top());
min_heap.pop();
}
ActionInterface
and Action
look like overkill. Why not just pass a suitable invocable object? It can hold a reference to a result
value which it updates.
Why does Stream
have a buffer
member that is only written and never read?
Why do we name the actions, but never use those names for anything?
The for
loop in solve
looks like it could be a std::transform()
.
Simplified code
Based on the above observations, I reduced to this version, which gives me no compiler warnings and no memory issues in Valgrind:
#include <concepts>
#include <functional>
#include <queue>
#include <string>
#include <vector>
namespace Solution
{
std::vector<double> solve(const std::vector<int>&);
}
template <typename T>
class Stream
{
std::vector<std::function<void(T)>> actions = {};
public:
void addAction(std::invocable<T> auto action)
{
actions.emplace_back(std::move(action));
}
void addElement(const T value)
{
for (const auto& action: actions) {
action(value);
}
}
};
template<typename T>
class RunningMedian
{
std::priority_queue<T> max_heap = {};
std::priority_queue<T,std::vector<T>,std::greater<T>> min_heap = {};
T& median;
public:
RunningMedian(T& median)
: median{median}
{}
void operator()(const T element)
{
if (element < median)
max_heap.push(element);
else
min_heap.push(element);
if (max_heap.size() > min_heap.size() + 1) {
min_heap.push(max_heap.top());
max_heap.pop();
} else if (min_heap.size() > max_heap.size() + 1) {
max_heap.push(min_heap.top());
min_heap.pop();
}
median
= min_heap.size() < max_heap.size() ? max_heap.top()
: max_heap.size() < min_heap.size() ? min_heap.top()
: (max_heap.top() + min_heap.top()) / 2;
}
};
std::vector<double> Solution::solve(const std::vector<int>& input)
{
Stream<int> stream{};
double median = 0.0;
stream.addAction(RunningMedian<double>{median});
std::vector<double> result;
result.reserve(input.size());
for (auto i: input) {
stream.addElement(i);
result.push_back(median);
}
return result;
}
// Simple test. TODO: more rigorous testing
#include <iostream>
int main()
{
std::vector a = { 1, 1, 1, 2, 3, 4, 5, 6, 5, 5};
const char* sep = "";
for (auto i: Solution::solve(a)) {
std::cout << sep << i;
sep = ", ";
}
std::cout << '\n';
}