Firstly, I apologize for the lengthy post. I was not sure which code I should include.
In practice of c++, I am writing a toy application for pub/sub using c++17
. To keep things simple, both the publisher and subscriber are on the same OS Process, and thus only a deque is needed to store messages.
I don't use a Singleton, but rather I create a shared_ptr
and std::move
it across constructors.
I attempt to use c++17
idioms where ever they are known to me; for example in std::scoped_lock
vs std::lock_guard
, and lambda over std::bind
. If there are any other places in the code that can be upgraded, please note it.
One of my main struggles with c++ is the lack of clarity on which techniques are c++11
and which are more modern
.
Along these lines, is any aware why the Google C++ style guide prohibits the use of c++20
? Especially since c++23
is fast approaching.
The first class acts as the Queue
, with call back mechanism.
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <mutex>
#include <deque>
#include <thread>
#include <functional>
namespace tareeqav {
namespace messaging {
class QueueBase {
public:
virtual ~QueueBase() {};
};
template<class T>
class Queue : public QueueBase {
public:
Queue() {
std::thread t = std::thread([this]() {
this->RunCallbacks();
});
t.detach();
};
void EnqueueMessage(std::shared_ptr<T const> message);
const int length() { return messages_.size(); };
template<typename CallbackT>
void RegisterCallback(CallbackT &&callback);
void RunCallbacks();
private:
std::mutex mutex_;
std::string id_;
std::deque<std::shared_ptr<T const>> messages_ = {};
std::vector<std::function<void(std::shared_ptr<T const>)>> callbacks_ = {};
std::chrono::time_point<std::chrono::system_clock> prev = std::chrono::system_clock::now();
std::shared_ptr<T const> DequeueMessage();
};
template<class T>
void Queue<T>::EnqueueMessage(std::shared_ptr<T const> message) {
messages_.push_back(message);
};
template<class T>
std::shared_ptr<T const> Queue<T>::DequeueMessage() {
if (length() > 0) {
auto item = messages_.front();
messages_.pop_front();
return item;
}
return nullptr;
};
template<class T>
template<typename CallbackT>
void Queue<T>::RegisterCallback(CallbackT &&callback) {
std::scoped_lock lock(mutex_);
callbacks_.template emplace_back(std::forward<CallbackT>(callback));
};
template<class T>
void Queue<T>::RunCallbacks() {
while (true) {
std::chrono::time_point<std::chrono::system_clock> now = std::chrono::system_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(prev - now);
auto ms = 100 - milliseconds.count();
while (ms > 0) {
--ms;
}
if (callbacks_.size() > 0 && messages_.size() > 0) {
std::scoped_lock lock(mutex_);
std::shared_ptr<T const> message = DequeueMessage();
for (auto &&callback: callbacks_) {
std::thread t = std::thread([&callback, &message]() {
callback(message);
});
t.detach();
}
}
prev = std::chrono::system_clock::now();
}
}
} // namespace messaging
} // namespace tareeqav
The Broker
class acts as a middle layer between the pub/sub and the stored data.
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <unordered_map>
#include "tareeqav/messaging/queue.h"
#include "tareeqav/messaging/proto/control_message.h"
namespace tareeqav {
namespace messaging {
enum class TopicStatus {
TOPIC_CREATED,
TOPIC_EXISTS
};
enum class MessageStatus {
QUEUED,
PENDING,
PUBLISHED,
TOPIC_NOT_FOUND
};
class Broker {
public:
template<typename T>
TopicStatus CreateTopic(std::string topicName);
template<typename T>
MessageStatus PublishMessage(std::string topicName, std::shared_ptr<T const> message);
template<class T, typename CallbackT>
void AddSubscription(
std::string topicName,
CallbackT &&callback);
private:
template<typename T>
std::shared_ptr<Queue<T const>> get(std::string topicName) {
auto base = queues_[topicName];
return std::dynamic_pointer_cast<Queue<T const>>(base);
}
std::vector<std::thread::id> threads_;
std::unordered_map<std::string, std::shared_ptr<QueueBase>> queues_;
};
template<typename T>
TopicStatus Broker::CreateTopic(std::string topicName) {
// check if we already have a topic with that name
if (queues_.find(topicName) == queues_.end()) {
queues_[topicName] = std::make_shared<Queue<T const>>();
return TopicStatus::TOPIC_CREATED;
}
return TopicStatus::TOPIC_EXISTS;
};
template<typename T>
MessageStatus Broker::PublishMessage(std::string topicName, std::shared_ptr<T const> message) {
if (queues_.find(topicName) != queues_.end()) {
auto queue = get<T>(topicName);
std::scoped_lock lock(queue->mutex_);
queue->EnqueueMessage(message);
return MessageStatus::QUEUED;
}
return MessageStatus::TOPIC_NOT_FOUND;
}
template<class T, typename CallbackT>
void Broker::AddSubscription(
std::string topicName,
CallbackT &&callback) {
if (queues_.find(topicName) != queues_.end()) {
auto queue = get<T>(topicName);
queue->RegisterCallback(std::forward<CallbackT>(callback));
}
}
} // namespace messaging
} // namespace tareeqav
The Subscriber
class is a simple example of the management of adding subscriptions.
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <utility>
#include "tareeqav/messaging/broker.h"
namespace tareeqav {
namespace nodes {
using tareeqav::messaging::Broker;
using tareeqav::messaging::TopicStatus;
using tareeqav::messaging::MessageStatus;
class Subscriber {
public:
Subscriber(
std::shared_ptr<Broker> broker,
std::string topicName);
template<class T, typename CallbackT>
void AddSubscription(CallbackT &&callback);
private:
const std::string topicName_;
std::shared_ptr<Broker> broker_;
};
Subscriber::Subscriber(
std::shared_ptr<Broker> broker,
std::string topicName) :
topicName_(std::move(topicName)), broker_(std::move(broker)) {
};
template<class T, class CallbackT>
void Subscriber::AddSubscription(CallbackT &&callback) {
broker_->template AddSubscription<T>(
topicName_,
std::forward<CallbackT>(callback));
}
} // namespace node
} // namespace tareeqav
The Publisher
class creates messages.
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <utility>
#include "tareeqav/messaging/broker.h"
namespace tareeqav {
namespace nodes {
using tareeqav::messaging::Broker;
using tareeqav::messaging::TopicStatus;
using tareeqav::messaging::MessageStatus;
template<class T>
class Publisher {
public:
Publisher(
std::shared_ptr<Broker> broker,
std::string topicName);
void CreateTopic() {
TopicStatus status = broker_->template CreateTopic<T>(topicName_);
switch (status) {
case TopicStatus::TOPIC_CREATED:
std::cout << "topic created" << std::endl;
break;
case TopicStatus::TOPIC_EXISTS:
std::cout << "topic exists" << std::endl;
break;
}
}
MessageStatus PublishMessage(
std::string topicName,
std::shared_ptr<const T> message);
private:
const std::string topicName_;
std::shared_ptr<Broker> broker_;
};
template<class T>
Publisher<T>::Publisher(
std::shared_ptr<Broker> broker,
std::string topicName):
topicName_(std::move(topicName)), broker_(std::move(broker)) {
};
template<class T>
MessageStatus Publisher<T>::PublishMessage(
const std::string topicName,
std::shared_ptr<T const> message) {
std::cout << "publishing message " << message << std::endl;
broker_->template PublishMessage(std::move(topicName), std::move(message));
return MessageStatus::QUEUED;
};
} // namespace node
} // namespace tareeqav
The base pipeline class in inherited below
#pragma once
#include <memory>
#include <utility>
#include "tareeqav/messaging/broker.h"
namespace tareeqav {
using tareeqav::messaging::Broker;
class Pipeline {
public:
Pipeline() = delete;
Pipeline(std::shared_ptr<Broker> broker) : broker_(std::move(broker)) {};
protected:
std::shared_ptr<Broker> broker_ = nullptr;
};
}
This pipeline is a consumer of the test message. A pipeline can be both a consumer and producer of messsages.
#pragma once
#include <memory>
#include "tareeqav/pipelines/pipeline.h"
#include "tareeqav/messaging/proto/control_message.h"
#include "tareeqav/pipelines/perception/control_subscriber.h"
namespace tareeqav {
namespace perception {
using std::placeholders::_1;
class PerceptionPipeline : public Pipeline {
public:
PerceptionPipeline(const std::shared_ptr<Broker> broker) : Pipeline(std::move(broker)) {
std::string topicName = std::string("/control_message");
subscriber_ = std::make_shared<
nodes::Subscriber>(broker, topicName);
controlCallback = std::make_shared<ControlSubscriber>();
subscriber_->AddSubscription<const messaging::ControlMessage>(
[this](auto &&T) { controlCallback->MessageCallback(std::forward<decltype(T)>(T)); }
);
};
private:
std::shared_ptr<nodes::Subscriber> subscriber_ = nullptr;
std::shared_ptr<ControlSubscriber> controlCallback = nullptr;
};
} // namespace perception
} // namespace tareeqav
This pipeline publishes a message.
#pragma once
#include "tareeqav/pipelines/pipeline.h"
#include "tareeqav/node/publisher.h"
#include "tareeqav/messaging/proto/control_message.h"
namespace tareeqav {
namespace planning {
class PlanningPipeline : public Pipeline {
public:
PlanningPipeline(std::shared_ptr<Broker> broker) : Pipeline(std::move(broker)) {
std::string topicName = std::string("/control_message");
controlPublisher_ = std::make_shared<nodes::Publisher<messaging::ControlMessage>>(
broker_, topicName);
controlPublisher_->CreateTopic();
auto message = std::make_shared<messaging::ControlMessage>(.9, .1);
controlPublisher_->PublishMessage(topicName, message);
};
private:
std::shared_ptr<nodes::Publisher<messaging::ControlMessage>> controlPublisher_ = nullptr;
};
} // namespace perception
} // namespace tareeqav
The control message (which will be replaced with protobuf soon)
#pragma once
namespace tareeqav
{
namespace messaging
{
class ControlMessage
{
public:
ControlMessage(){};
ControlMessage(float a, float s): acceleration(a), steering(s){};
float acceleration;
float steering;
};
} // namespace perception
} // namespace tareeqav
This is the driver with the entry point.
#include <memory>
#include "tareeqav/messaging/broker.h"
#include "tareeqav/pipelines/planning/planning.h"
#include "tareeqav/pipelines/perception/perception.h"
using tareeqav::messaging::Broker;
using tareeqav::planning::PlanningPipeline;
using tareeqav::perception::PerceptionPipeline;
int main(int argc, char *argv[]) {
auto broker = std::make_shared<Broker>();
auto planningPipeline = std::make_shared<PlanningPipeline>(PlanningPipeline(broker));
auto perceptionPipeline = std::make_shared<PerceptionPipeline>(PerceptionPipeline(broker));
while (true) {};
return 0;
}
Pipeline
class appears to be missing. (If you’re not sure which code to include, a good rule of thumb is to include everything that reviewers will need to compile your test program… because that’s what a lot of them are going to want to do. If there’s far too much for a post, you could always link to the project, but in this case that shouldn’t be a problem.) \$\endgroup\$Pipeline
class and theControlMessage
class \$\endgroup\$