Structured types (structs) and functions are fundamental building blocks in MQL5, enabling developers to organize data logically and encapsulate reusable code. Understanding how to effectively combine these features, particularly by returning structs from functions, is crucial for writing clean, maintainable, and efficient MQL5 programs, whether you’re building Expert Advisors, custom indicators, or utility scripts.
What is a Struct in MQL5?
A struct (structure) in MQL5 is a user-defined data type that groups together variables of different data types under a single name. This allows you to treat related pieces of data as a single unit. For instance, a struct could hold all parameters for a trading order (symbol, type, volume, price, stop loss, take profit), or configuration settings for an indicator.
Structs enhance code readability and manageability by logically bundling data. Unlike MQL4, MQL5 provides full support for structs, including nesting and methods.
struct TradeParameters
{
string symbol;
ENUM_ORDER_TYPE type;
double volume;
double price;
double stoploss;
double takeprofit;
};
Basics of Functions in MQL5
Functions in MQL5 are blocks of code that perform a specific task. They can accept parameters (inputs) and can return a value (output) of a specified data type. Functions promote modularity, allowing you to break down complex problems into smaller, manageable pieces, improving code reusability and reducing redundancy.
The basic syntax involves a return type, function name, and a list of parameters:
double CalculateProfit(double entryPrice, double exitPrice, double volume)
{
// Calculation logic
return profit;
}
Why Return a Struct from a Function?
Returning a struct from a function is particularly useful when a function needs to compute or retrieve multiple related values that logically belong together. Instead of using multiple output parameters passed by reference, or returning a complex array, returning a struct bundles these values into a single, named entity. This improves:
- Clarity: The function’s signature clearly indicates that it returns a set of related data.
- Simplicity: The caller receives all related data in one go.
- Maintainability: If the set of related data changes, you only need to update the struct definition and the function, rather than potentially many function signatures or parameter lists.
This capability is a significant advantage of MQL5’s richer type system compared to MQL4’s more limited options for returning multiple values.
Returning a Struct by Value
The simplest way to return a struct is by value. When you return a struct by value, a copy of the entire struct is created and returned to the caller. This is straightforward but can incur a performance overhead for large structs due to the copying process.
Defining a Struct for Return
First, you define the structure that will hold the data you intend to return. Let’s refine our TradeParameters example:
struct TradeParameters
{
string symbol;
ENUM_ORDER_TYPE type;
double volume;
double price;
double stoploss;
double takeprofit;
int magicNumber; // Added for clarity
};
Implementing the Function
Implement a function with the struct type as its return type. Inside the function, create an instance of the struct, populate its members, and then return the struct instance.
TradeParameters GetDefaultTradeParameters()
{
TradeParameters params; // Create a struct instance
params.symbol = _Symbol;
params.type = ORDER_TYPE_BUY;
params.volume = 0.1;
params.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
params.stoploss = params.price - 100 * _Point;
params.takeprofit = params.price + 200 * _Point;
params.magicNumber = 12345;
return params; // Return the struct instance
}
Accessing Struct Members
In the calling code, you declare a variable of the struct type and assign the result of the function call to it. You can then access individual members using the dot (.) operator.
void OnStart()
{
TradeParameters myTradeSettings = GetDefaultTradeParameters();
Print("Symbol: ", myTradeSettings.symbol);
Print("Volume: ", myTradeSettings.volume);
Print("Magic: ", myTradeSettings.magicNumber);
// Use these parameters for placing an order, for example
// MqlTradeRequest request;
// request.symbol = myTradeSettings.symbol;
// ... and so on
}
Example: Returning Trade Parameters
The example above demonstrates returning default trade parameters. A more dynamic function might take some inputs and compute parameters based on those inputs.
// Assume TradeParameters struct is defined
TradeParameters CalculateOptimalTrade(string asset, ENUM_TIMEFRAME timeframe, double riskPercent)
{
TradeParameters calculatedParams;
calculatedParams.symbol = asset;
calculatedParams.type = ORDER_TYPE_BUY; // Simplified
calculatedParams.volume = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / 100.0 / 10.0; // Simplified vol calc
calculatedParams.price = SymbolInfoDouble(asset, SYMBOL_ASK);
// Add logic to calculate SL/TP based on timeframe/ATR/etc.
calculatedParams.stoploss = calculatedParams.price - 50 * SymbolInfoDouble(asset, SYMBOL_POINT);
calculatedParams.takeprofit = calculatedParams.price + 100 * SymbolInfoDouble(asset, SYMBOL_POINT);
calculatedParams.magicNumber = 56789;
return calculatedParams;
}
void OnTick()
{
if (Bars(_Symbol, PERIOD_M15) < 100) return; // Example condition
TradeParameters tradePlan = CalculateOptimalTrade(_Symbol, PERIOD_M15, 1.0);
Print("Planning trade for ", tradePlan.symbol, ": Volume=", tradePlan.volume, ", SL=", tradePlan.stoploss);
// Proceed to placing order using tradePlan
}
Returning a Struct by Pointer
Returning a struct by pointer means the function returns the memory address where the struct data resides, rather than a copy of the struct itself. This can be more efficient for large structs as it avoids the overhead of copying. However, it introduces complexities related to memory management.
Why Use Pointers to Return Structs?
- Efficiency: Avoids copying potentially large amounts of data, which can be faster, especially in performance-critical sections of code or when the struct is large.
- Flexibility: Allows the function to return a struct that persists beyond the function’s scope, provided the memory is properly managed.
Declaring a Function Returning a Struct Pointer
The return type of the function is a pointer to the struct type, denoted by appending an asterisk (*) to the struct name.
struct MarketData
{
double ask;
double bid;
double last;
long volume;
datetime time;
};
MarketData* GetCurrentMarketData(string symbol);
Memory Management Considerations
When a function returns a pointer to a struct, that struct instance must be created dynamically using the new operator. This allocates memory on the heap. The calling code receives the address of this dynamically allocated memory.
Crucially, the caller becomes responsible for deallocating this memory when it’s no longer needed using the delete operator to prevent memory leaks. This is a significant difference compared to returning by value, where memory management is handled automatically.
MarketData* GetCurrentMarketData(string symbol)
{
MarketData* data = new MarketData(); // Dynamically allocate memory
if(data == NULL)
{
Print("Failed to allocate memory for MarketData");
return NULL; // Return NULL on failure
}
data.ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
data.bid = SymbolInfoDouble(symbol, SYMBOL_BID);
data.last = SymbolInfoDouble(symbol, SYMBOL_LAST);
data.volume = SymbolInfoInteger(symbol, SYMBOL_VOLUME);
data.time = TimeCurrent();
return data; // Return the pointer
}
// ... later, in calling code:
MarketData* currentData = GetCurrentMarketData(_Symbol);
if (currentData != NULL)
{
Print("Current Ask: ", currentData.ask);
Print("Current Bid: ", currentData.bid);
// *** IMPORTANT: Deallocate memory when done ***
delete currentData;
currentData = NULL; // Best practice to set pointer to NULL after deletion
}
Failing to delete dynamically allocated structs results in memory leaks, which can degrade terminal performance over time.
Example: Returning a Struct Representing Market Data
Building on the previous definitions:
// Assume MarketData struct and GetCurrentMarketData function are defined
void OnTick()
{
static datetime lastProcessedTime = 0; // Prevent excessive calls
if (TimeCurrent() == lastProcessedTime) return;
lastProcessedTime = TimeCurrent();
MarketData* data = GetCurrentMarketData(_Symbol);
if (data != NULL)
{
Print("Market Data for ", _Symbol);
Print(" Time: ", TimeToString(data.time, TIME_SECONDS));
Print(" Ask: ", data.ask);
Print(" Bid: ", data.bid);
Print(" Last: ", data.last);
Print(" Volume: ", data.volume);
// *** MUST delete the allocated memory ***
delete data;
data = NULL;
}
}
This pattern is useful when you need to fetch market details as a single package. Remember the delete call is essential.
Practical Examples and Use Cases
Returning structs from functions makes your MQL5 code more organized and easier to understand by grouping related outputs.
Example 1: Function Returning Order Information
Suppose you have a complex process to find a specific open order and want to return its key details. A struct is ideal for this.
struct OrderInfo
{
long ticket;
string symbol;
ENUM_ORDER_TYPE type;
double volume;
double price;
double stoploss;
double takeprofit;
long magic;
};
// Function to find and return details of the first order with a specific magic number
bool GetOrderByMagic(long magic, OrderInfo& order)
{
// Note: This example uses a reference parameter instead of returning a struct
// directly by value or pointer, which is another common pattern for
// returning multiple values, often combined with a boolean success indicator.
// However, for demonstrating struct return, we can adapt it:
// ** Returning by value (simplified - assumes order found):
/*
OrderInfo FindOrderByMagicValue(long magic)
{
OrderInfo foundOrder;
// Loop through orders... if found...
// foundOrder.ticket = order_ticket;
// ... populate members...
// return foundOrder;
// If not found, what to return? A default struct might not be suitable.
// This highlights a limitation of returning by value when the result might not exist.
}
*/
// ** Returning by pointer (better for cases where the result might not exist):
/*
OrderInfo* FindOrderByMagicPointer(long magic)
{
// Loop through orders
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (OrderSelect(i, SELECT_BY_POS, ORDER_TIME_OPEN))
{
if (OrderMagic() == magic)
{
OrderInfo* foundOrder = new OrderInfo();
if (foundOrder != NULL)
{
foundOrder.ticket = OrderTicket();
foundOrder.symbol = OrderSymbol();
foundOrder.type = OrderType();
foundOrder.volume = OrderVolume();
foundOrder.price = OrderOpenPrice();
foundOrder.stoploss = OrderStopLoss();
foundOrder.takeprofit = OrderTakeProfit();
foundOrder.magic = OrderMagic();
return foundOrder; // Return pointer to found order info
}
}
}
}
return NULL; // Return NULL if no order is found
}
// Usage:
void OnTick()
{
OrderInfo* myOrderDetails = FindOrderByMagicPointer(12345);
if (myOrderDetails != NULL)
{
Print("Found order ", myOrderDetails.ticket, " for ", myOrderDetails.symbol);
// Use details...
delete myOrderDetails; // *** Don't forget! ***
myOrderDetails = NULL;
}
}
*/
// The initial function signature using a reference parameter is often more robust
// when you need to indicate success/failure alongside returning data:
// This is a common alternative pattern, though not strictly "returning a struct".
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (OrderSelect(i, SELECT_BY_POS, ORDER_TIME_OPEN))
{
if (OrderMagic() == magic)
{
order.ticket = OrderTicket();
order.symbol = OrderSymbol();
order.type = OrderType();
order.volume = OrderVolume();
order.price = OrderOpenPrice();
order.stoploss = OrderStopLoss();
order.takeprofit = OrderTakeProfit();
order.magic = OrderMagic();
return true; // Success
}
}
}
return false; // Not found
}
// Usage of the reference version:
void OnTick()
{
OrderInfo myOrder;
if (GetOrderByMagic(12345, myOrder))
{
Print("Found order ", myOrder.ticket, " volume ", myOrder.volume);
// myOrder exists within the caller's scope, no delete needed
}
}
This example illustrates that while returning a struct (especially by pointer) is viable, passing a struct by reference is often a preferred pattern in MQL5 when a function might fail to produce a result, allowing the function to return a boolean status.
Example 2: Function Returning Account Summary
Summarizing multiple account properties is another use case.
struct AccountSummary
{
double balance;
double equity;
double profit;
double freeMargin;
string currency;
};
AccountSummary GetAccountSnapshot()
{
AccountSummary summary;
summary.balance = AccountInfoDouble(ACCOUNT_BALANCE);
summary.equity = AccountInfoDouble(ACCOUNT_EQUITY);
summary.profit = AccountInfoDouble(ACCOUNT_PROFIT);
summary.freeMargin = AccountInfoDouble(ACCOUNT_FREEMARGIN);
summary.currency = AccountInfoString(ACCOUNT_CURRENCY);
return summary; // Return by value is simple here
}
void OnTick()
{
AccountSummary currentAccount = GetAccountSnapshot();
Print("Balance: ", currentAccount.balance, " ", currentAccount.currency);
Print("Equity: ", currentAccount.equity, " ", currentAccount.currency);
}
Returning by value is suitable here as the struct is relatively small and the data is always available.
Combining Structs and Functions for Modular Code
Using structs as return types encourages breaking down complex tasks into smaller, more manageable functions. Each function can be responsible for calculating or fetching a specific set of related data, packaged neatly within a struct. This enhances modularity, testability, and makes the overall code structure easier to understand and maintain. You can have functions like GetTradeConfig(), AnalyzeMarketConditions(), GetPositionDetails(), all returning relevant structs.
Best Practices and Common Pitfalls
Using structs with functions is powerful, but requires attention to detail, especially regarding memory and structure.
Memory Leaks and Pointer Management
The most critical pitfall when returning structs by pointer is failing to manage dynamically allocated memory. Every new must have a corresponding delete. In EAs and indicators, this is particularly important in event handlers (OnTick, OnTimer, OnChartEvent) where functions returning pointers might be called repeatedly. Use OnInit, OnDeinit, OnTimer (for cleanup) carefully.
A pattern to mitigate this is to return NULL on allocation failure and always check the pointer is not NULL before accessing members or attempting deletion.
Code Readability and Maintainability
- Use meaningful names for structs and their members.
- Document the purpose of structs and functions returning them.
- Keep structs focused on a single logical entity.
- Consider whether returning by value, pointer, or using a reference parameter is most appropriate for the specific task (success/failure indication, size of data, lifetime requirements).
Error Handling When Returning Structs
- By Value: Error handling is less direct. You might need a special “invalid” state within the struct (e.g., a
bool isValidmember), or rely on checking specific returned values (like a specialticket = -1). - By Pointer: Returning
NULLis the standard way to indicate failure (e.g., allocation failed, item not found). The caller must check forNULLbefore dereferencing the pointer. - By Reference (alternative): As shown in the
GetOrderByMagicexample, returning a boolean (truefor success,falsefor failure) and populating a struct passed by reference is a very common and robust error-handling pattern in MQL5 for functions that might not yield a result.
When to Use Value vs. Pointer Return
- Return by Value:
- Suitable for small structs.
- When the returned data is always valid/available.
- When the data’s lifetime is limited to the scope of the function call.
- Simplifies memory management (it’s automatic).
- Return by Pointer:
- Suitable for large structs where copying is expensive.
- When the data needs to persist beyond the function’s immediate scope (though this needs careful management).
- When the function might fail to produce a result (return
NULL). - Requires manual memory management (
new/delete) and careful error checking (NULLchecks).
Generally, prefer returning by value or using reference parameters unless there is a clear performance benefit or requirement for dynamic allocation and controlled lifetime provided by returning a pointer.