Signal sender cast in QT
QT implies that the client code will do qobject_cast that is actually dynamic_cast:
awl::ProcessTask<void> MarketModel::coPlaceOrder(OrderPtr p)
{
// Update SQLite databse...
// ...
// QT signals are used in both C++ and QML.
// They works with QObject*, and they are not aware of concrete types.
QObject::connect(p.get(), &OrderModel::statusChanged, this, &MarketModel::onOrderStatusChanged);
}
void MarketModel::onOrderStatusChanged()
{
// When a signal is emitted sender() holds its sender as QObject*.
OrderPtr p = qobject_cast<OrderModel*>(sender())->shared_from_this();
// Working with concrete type OrderModel*.
switch (p->data().status)
{
case data::OrderStatus::Pending:
break;
case data::OrderStatus::Open:
// ...
}
Event sender cast in WPF
A typical WPF event handler in C# looks like this:
private void okButton_Click(object sender, RoutedEventArgs e)
{
if (topLevelProjectCheckBox.IsChecked ?? false)
{
ProjectRow.ProjectRowParent = null;
}
DialogResult = true;
}
an example of a type cast from my old code in C#:
public class DbList<T> : IList<T>, System.Collections.IList, INotifyCollectionChanged
where T : class
{
//..
T Object2T(object value)
{
T t = value as T;
if (t == null)
{
throw new ArgumentException(
String.Format("The value '{0}' is not of type '{1}' and cannot be used in this generic collection.",
value, typeof(T).Name));
}
return t;
}
void PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (Dispatcher.CheckAccess())
{
T item = Object2T(sender);
bool should_exist = Filter(item);
int index = this.IndexOf(item);
bool exists = index != -1;
if (should_exist != exists)
{
if (should_exist)
{
this.InternalAdd(item);
}
else
{
this.RemoveAt(index);
}
}
}
else
{
Dispatcher.Invoke(
new System.ComponentModel.PropertyChangedEventHandler(PropertyChanged),
sender, e);
}
}
}
An example of violation of LSP with dynamic_cast
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <cassert>
/// Base interface — contract: any Logger can log messages.
struct Logger {
virtual ~Logger() = default;
virtual void log(std::string_view msg) = 0;
};
/// Simple console implementation.
struct ConsoleLogger : Logger {
void log(std::string_view msg) override {
std::cout << "[console] " << msg << '\n';
}
};
/// File logger with an extra feature: rotation.
struct FileLogger : Logger {
void rotate_now() { rotated_ = true; } // Additional behavior not in base contract
void log(std::string_view msg) override { (void)msg; /* write to file here */ }
bool rotated_ = false;
};
/// Client code — decides to treat FileLogger specially using dynamic_cast.
void write_startup(Logger& lg) {
if (auto* fl = dynamic_cast<FileLogger*>(&lg)) {
// Special-case for a specific subtype.
fl->rotate_now(); // <-- strengthens preconditions for FileLogger
}
lg.log("service start");
}
int main() {
ConsoleLogger c;
FileLogger f;
write_startup(c); // Works fine
write_startup(f); // Extra behavior for subtype
assert(f.rotated_); // Behavior now depends on the concrete type → violates LSP
}
❌ Why this violates LSP
- The client code branches on the subtype and changes the contract:
The baseLoggerpromised “you can log messages,” but the client now assumes
“if it’s a FileLogger, you must also rotate first.” - Replacing
FileLoggerwith another subclass (e.g.,DatabaseLogger)
will change the behavior or break expectations. - The client depends on implementation details of a specific subtype.
String-based Logger Factory (LSP-compliant)
#include <iostream>
#include <fstream>
#include <memory>
#include <string>
#include <stdexcept>
/// Base interface — all loggers must support this.
struct Logger {
virtual ~Logger() = default;
virtual void log(std::string_view msg) = 0;
};
/// Writes logs to the console.
struct ConsoleLogger : Logger {
void log(std::string_view msg) override {
std::cout << "[console] " << msg << '\n';
}
};
/// Writes logs to a file.
struct FileLogger : Logger {
explicit FileLogger(const std::string& filename)
: file_(filename, std::ios::app)
{
if (!file_)
throw std::runtime_error("Failed to open log file: " + filename);
}
void log(std::string_view msg) override {
file_ << "[file] " << msg << '\n';
}
private:
std::ofstream file_;
};
/// Factory that creates a Logger by string name.
/// Supported: "console", "file"
std::unique_ptr<Logger> make_logger(const std::string& name, const std::string& param = "")
{
if (name == "console") {
return std::make_unique<ConsoleLogger>();
}
else if (name == "file") {
if (param.empty())
throw std::invalid_argument("File logger requires a filename");
return std::make_unique<FileLogger>(param);
}
else {
throw std::invalid_argument("Unknown logger type: " + name);
}
}
/// Function that creates a logger by name and writes startup messages.
/// It depends only on the Logger interface — no LSP violations.
void write_startup(const std::string& logger_name, const std::string& param = "")
{
try {
// 1. Create a logger (could be console, file, network, etc.)
auto logger = make_logger(logger_name, param);
// 2. Use it through the base interface only
logger->log("=== Application startup ===");
logger->log("Initializing subsystems...");
logger->log("Startup complete.");
}
catch (const std::exception& ex) {
std::cerr << "Failed to write startup log: " << ex.what() << '\n';
}
}
int main()
{
// Writes to console
write_startup("console");
// Writes to file
write_startup("file", "startup.log");
// Uncomment to see error handling
// write_startup("unknown");
return 0;
}
✅ Why this still satisfies LSP
- Both
ConsoleLoggerandFileLoggerimplement the same interfaceLogger. - The factory hides subtype knowledge.
- The client code (
run_service) treats every logger uniformly. - Adding a new subtype (e.g.
"network","syslog") doesn’t change the client — only the factory.
We still use different types of Logger inside write_startup method depending on name parameter, right? Is it a hidden dynamic_cast? Or a dynamic_cast is a bit different style? Does make_logger violates LSP?
Why dynamic_cast is not a factory?
The factory as a parameter
#include <iostream>
#include <fstream>
#include <memory>
#include <string>
#include <stdexcept>
/// Base interface — any logger can write messages somewhere.
struct Logger {
virtual ~Logger() = default;
virtual void log(std::string_view msg) = 0;
};
/// Writes to console.
struct ConsoleLogger : Logger {
void log(std::string_view msg) override {
std::cout << "[console] " << msg << '\n';
}
};
/// Writes to a file.
struct FileLogger : Logger {
explicit FileLogger(const std::string& filename)
: file_(filename, std::ios::app)
{
if (!file_)
throw std::runtime_error("Failed to open log file: " + filename);
}
void log(std::string_view msg) override {
file_ << "[file] " << msg << '\n';
}
private:
std::ofstream file_;
};
/// Interface for factories that create loggers.
/// Different factory implementations can decide *how* to create them.
struct LoggerFactory {
virtual ~LoggerFactory() = default;
/// Factory method: creates a logger by name (e.g. "console", "file")
virtual std::unique_ptr<Logger> make_logger(
const std::string& name,
const std::string& param = "") const = 0;
};
/// Default implementation of LoggerFactory.
/// Encapsulates creation logic for known types.
struct DefaultLoggerFactory : LoggerFactory {
std::unique_ptr<Logger> make_logger(
const std::string& name,
const std::string& param = "") const override
{
if (name == "console") {
return std::make_unique<ConsoleLogger>();
} else if (name == "file") {
if (param.empty())
throw std::invalid_argument("File logger requires a filename");
return std::make_unique<FileLogger>(param);
} else {
throw std::invalid_argument("Unknown logger type: " + name);
}
}
};
/// High-level function that uses the factory to create a logger.
/// It doesn't depend on the specific factory implementation.
void write_startup(const LoggerFactory& factory,
const std::string& logger_name,
const std::string& param = "")
{
try {
// Create a logger using the factory interface
auto logger = factory.make_logger(logger_name, param);
// Use it through the base interface only (LSP preserved)
logger->log("=== Application startup ===");
logger->log("Initializing subsystems...");
logger->log("Startup complete.");
}
catch (const std::exception& ex) {
std::cerr << "Failed to write startup log: " << ex.what() << '\n';
}
}
int main()
{
DefaultLoggerFactory factory;
// Works for console
write_startup(factory, "console");
// Works for file
write_startup(factory, "file", "startup.log");
return 0;
}
Liskov Substitution Principle (LSP)
📜 Formal Definition (Barbara Liskov, 1987)
Objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program.
💡 In simpler terms:
If you have a base class Base and a derived class Derived,
then anywhere the program expects a Base,
you should be able to use a Derived instead —
and everything should still work correctly.
⚙️ What this really means
A subclass must preserve the behavior promised by the base class.
It can extend or specialize it, but not break it.
Formally:
If a property φ(x) is true for all objects x of type Base,
then φ(y) must also be true for all objects y of any subtype Derived.
🚫 Violations (breaking LSP)
A subclass violates LSP if it:
- Strengthens preconditions — it requires more than the base class did.
(e.g., “must callinitialize()before usingdoWork().”) - Weakens postconditions — it delivers less than the base class promised.
(e.g., base guarantees “always non-null,” derived may return null.) - Breaks invariants — it changes essential behavior or assumptions of the base type.
(e.g.,Squareinheriting fromRectanglebut redefining width/height rules.)
What does “correctness of the program” mean
“Correctness” means that when you replace a base class object with a derived class object,
the program still behaves according to its intended specification —
it still does the right thing, follows the same rules, and doesn’t break any expectations.
It’s not just about compiling or running —
it’s about logical and behavioral correctness.

